Node.js ApiServer

A ready to go, modular, multi transport, streaming friendly, JSON(P) API Server for Node.js

apiserver build status

A ready to go, modular, multi transport, streaming friendly, JSON(P) API Server.

Why use ApiServer and not restify or express?

Strong competitors I guess.

Express targets web applications providing support for templates, views, and all the facilities the you probably need if you're writing a web app. Restify let you "build "strict" API services" but it's too big and it concentrates on server to server API, that will not be consumed by your browser.

ApiServer is rad. It is a slim, fast, minimal API framework, built to provide you a flexible API consumable both in the browser and from other apps. It ships with JSON, JSONP (GET/POST) transports and a powerful fast routing engine OOTB. The source code is small, heavily tested and decoupled. Your API source will be well organized in context objects, allowing you to keep it in a meaningful maintainable way.

Killer features

Installation

⚡ npm install apiserver
var ApiServer = require('apiserver')

Class Methods

Class Method: constructor

Syntax:

new ApiServer([options])

Available Options:

{
  'cache-control': 'max-age=0, no-cache, no-store, must-revalidate',
  'expires': 0,
  'pragma': 'no-cache',
  'x-server': 'ApiServer v' + ApiServer.version + ' raging on nodejs ' + process.version
}

Example:

var https = require('https'),
    ApiServer = require('apiserver')

apiserver = new ApiServer({
  port: 80,
  server: https.createServer(),
  standardHeaders: {
    'cache-control': 'max-age=0, no-cache, no-store, must-revalidate',
    'x-awesome-field': 'awezing value'
  },
  timeout: 2000
})

Class Method: addModule

Adds a new module to to the current API set. It triggers the router.update method.

Syntax:

ApiServer.prototype.addModule(version, moduleName, apiModule)

Arguments:

Examples:

var apiserver = new ApiServer()
apiserver.addModule('v1', 'user', userModule)
apiserver.addModule('v1', 'pages', pageModule)
apiserver.addModule('v2', 'user', userModule2)

Class Method: use

Adds a middleware object to the middleware chain. It triggers the router.update method.

Each middleware is associated to a RegExp used to test the API end-point route. If the route matches the RegExp the middleware will be a part of the chain and will be executed.

Read more about middleware here.

Syntax:

ApiServer.prototype.use(route, middleware)

Arguments:

Examples:

var apiserver = new ApiServer()
apiserver.use(/./, new MyMiddleWare({ foo: 'bar', bar: true }))
apiserver.use(/(signin|signup)/, ApiServer.payloadParser())
apiserver.use(/^\/v1\/files\/upload$/, ApiServer.multipartParser())

Class Method: listen

Bind the server to a port

Syntax:

ApiServer.prototype.listen([port], [callback])

Arguments:

Example:

From this point on, all the examples will take the require statements as assumption

var apiserver = new ApiServer()
apiserver.listen(80, function (err) {
  if (err) {
    console.error('Something terrible happened: %s', err.message)
  } else {
    console.log('Successful bound to port %s', this.port)
  }
})

Class Method: close

Unbind the server from the current port

Syntax:

ApiServer.prototype.close([callback])

Arguments:

Example:

var apiserver = new ApiServer()
apiserver.listen(80, onListen)

function onListen(err) {
  if (err) {
    console.error('Something terrible happened: %s', err.message)
  } else {
    setTimeout(function () {
      apiserver.close(onClose)
    }, 5000)
  }
}

function onClose() {
  console.log('port unbound correctly')
}

Class Events

Class Event: requestEnd

Emitted when an API method closes the response, even with response.end.

Event data

apiserver.on('requestEnd', function (url, responseTime) {

})

Class Event: timeout

Emitted when an API method exceed the maximum allowed time (see timenout option), before closing the response.

Event data

apiserver.on('timenout', function (url) {

})

Class Event: error

Emitted when a sync error is triggered during the middleware chain execution, can be both your API, a transport or a simple middleware.

You still have to deal with async errors

Event data

apiserver.on('error', function (url, err) {

})

Modules

A module is a set of API end-points grouped in the same context:

Modules Interface

Each module method (API end-point) must implement this interface and expect request and response parameters

function (request, response)

The request object is "extendend" ootb with the following members:

As you can see, there is no callback to call, you have to deal directly with the response.

Take a look at your transport documentation and use the right method that ships within the response object. You can also roughly close and write to the response stream in an edge case.

Modules Examples

Object literal

var apiserver = new ApiServer()
var userModule = {
  signin: function (request, response) {
    // rough approach
    response.writeHead(200)
    response.end('ok')
  },
  signout: function (request, response) {
    // JSON transport
    response.serveJSON({ foo: 'bar' })
  }
}
apiserver.addModule('v1', 'user', userModule)

Class

var apiserver = new ApiServer()
var UserModule = function (options) {
  this.database = options.database
  this.serviceName = options.serviceName
}

UserModule.prototype.signin = function (request, response) {
  var self = this
  self.database.searchUser(request.querystring.username, function (err) {
    if (err) {
      response.serveJSON({ success: true, err: err.message })
    } else {
      response.serveJSON({ success: true, message: 'welcome to ' + self.serviceName })
    }
  })
}

UserModule.prototype.signout = function (request, response) {
  // you can use the response as usual
  // a redirect for example
  response.writeHead(302, {
    'location': 'http://example.org/logout_suceesful'
  })
  response.end()
}
var database = /* your db object*/
apiserver.addModule('v1', 'user', new UserModule(database, 'My Awesome Service'))

Middleware

The concept of middleware is not new at all, you can find the same pattern in visionmedia/express in mcavage/node-restify and in many others. A middleware is a piece of software that adds (or patches) a feature into another software. Usually there is a common interface to implement, because the caller software, in this case our ApiServer, should know how to interact with the middleware.

You should check out the source code for a large understanding, middleware is relatively easy to code.

Middleware Chain

The ApiServer uses kilianc/node-fnchain to execute all the active middleware and reach the API method (that actually is the last ring of the chain). This means that the order of the execution depends on the order you activated the middleware.

Each middleware can both exit with an error or explicitly stop the chain (not reaching your API method). This is useful in case of a precondition check (auth, sessions, DoS attack filter...), or just because you packed some shared code as middleware which must be executed before your API method.

At the middleware execution level, the response object is already patched with the default transport methods, so you can use these methods to write and close the response. Is a good practice to leave at the top of the chain the extra transports middleware.

// constructor adds the default transport automatically
var apiserver = new ApiServer()

// lets ad first our custom transports
apiserver.use(/\.xml$/, myXMLTransport())
apiserver.use(/\.csv$/, myCSVTransport())
apiserver.use(/\.yml$/, myYAMLTransport())

// now activate our middleware
apiserver.use(/form/, ApiServer.payloadParser())
apiserver.use(/upload/, ApiServer.multipartParser())

...

The request payload (the data event) is paused by default and can be resumed calling request.resume() at any level of execution: middleware, module, transport. Why? Because you should explicitly accept or refuse a payload, this way you will save memory not buffering useless data.

Take a look at both the pause and resume official docs.

ApiServer is using this patch to provide a robust buffered pause resume method, so you don't have do deal with the flying chunks after the pause call

Middleware Interface

Each middleware must implement this interface.

module.exports = function (options) {
  return function (request, response, next) {
    // do sometihng async and when you're done call the callback
    options.count++
    next()
  }
}

A middleware basically, is a function that returns another function, this one must declare 3 paramaters:

The next callback expects 2 parameters:

Transports

A transport is a particular middleware that "extends" the response object. It can provide new methods that allow you to serve your data to the client in different ways.

Usually this is how you send data back to the client:

function (request, response) {
  response.writeHead(200, {
    'content-type': 'application/json'
  })
  response.end(JSON.stringify({ foo: 'bar' }))
})

This is for example how the default JSONTransport simplify the process

function (request, response) {
  response.serveJSON({ foo: 'bar' })
})

Basically what a transport does, is to wrap your data around a meaningful format (JSON, JSONP, HTML, XML, CSV, ...) understandable by your clients. It takes care of all the small things that the raw response needs (headers, status codes, buffering, ...)

Transports must be at the top of the middleware chain, in order to allow other middleware to use them.

JSONTransport is the default one, is attached before the middleware chain execution and then is available at every level of execution. You don't need to allocate it directly, the server itself will allocate the transport passing as options the ApiServer constructor options object.

Example

module.exports = function (options) {
  function serve<FORMAT>(request, response, data, options) {
    response.writeHead(200, {
      'content-type': 'application/<FORMAT>'
    })
    response.end(<FORMAT>.stringify(data))
  }
  return function (request, response) {
    // attach some new method to the response
    response.serve<FORMAT> = serve<FORMAT>.bind(this, request, response)
  }
}

where <FORMAT> is the formatting method of your data.

Router

Apiserver uses apiserver-router as default router, a fast routing system with integrated caching. It basically translates your API methods names in routes, doing some convenient case conversion.

You can change the default behavior passing a custom router as router option in the Apiserver constructor.

Example

var UserModule = function (options) {
  this.options = options
}

// will be translated into /1/random_photo_module/create_album
UserModule.prototype.createAlbum = function (request, response) { ... }

// will be translated into /1/random_photo_module/upload_photo
UserModule.prototype.uploadPhoto = function (request, response) { ... }

// private method, skipped by the router
UserModule.prototype._checkFileExtension = function (request, response) { ... }

apiserver.addModule('1', 'randomPhotoModule', new UserModule())

N.B. the moduleName also will be translated

Router Interface

new Router()
Router.prototype.update(modules, middlewareList)
Router.prototype.get(pathname)

Bundled Middleware

JSONTransport

JSONTransport is the default transport bundled with ApiServer and we can call it the real killer feature.

It provides JSON and JSONP that work with both GET / POST methods.

Examples

// decontextualized API method
function (request, response) {
  response.serveJSON({ foo: 'bar' })
})
// decontextualized API method
function (request, response) {
  response.serveJSON(['foo','bar', ...], {
    httpStatusCode: 404,
    httpStatusMessage: 'maybe.. you\'re lost',
    headers: {
      'x-value': 'foo'
    }
  })
})
// decontextualized API method
function (request, response) {
  var count = 3
  var interval = setInterval(function () {
    if (count === 0) {
      clearInterval(interval)
      response.streamJSON()
    } else {
      count--
      response.streamJSON({ foo: 'bar' })
    }
  }, 200)
})

yields

[
   { "foo": "bar" },
   { "foo": "bar" },
   { "foo": "bar" }
]

Read the full docs here

payloadParser

The payload parser automatically resume() the payload, buffer it and parse it. It only works with PUT POST OPTIONS http methods, because they are the only that can carryout a payload by specs definition.

Two kinds of payload can be parsed:

The following attributes will be attached to the request object:

Syntax

ApiServer.payloadParser()

Example

var apiserver = new ApiServer()
apiserver.use(/1\/my_module\/my_method_api$/, ApiServer.payloadParser())
apiserver.addModule('1', 'myModule', {
  'my_method_api': function (request, response) {
    if (request.parseError) {
      // :(
      console.error(request.parseError.message)
    } else {
      request.body // an object containing the parsed data
      request.rawBody // contains a binary buffer with your payload
    }
  }
})

multipartParser

The multipart-parser automatically resume() the payload, and attach it to a felixge/node-formidable IncomingForm object. It only works with PUT POST OPTIONS http methods, because they are the only that can carryout a payload by specs definition.

Only a multipart/form-data payload is parsed and the following attribute will be attached to the request object:

Syntax

ApiServer.multipartParser()

Example

var apiserver = new ApiServer()
apiserver.use(/1\/my_module\/my_method_api$/, ApiServer.multipartParser())
apiserver.addModule('1', 'myModule', {
  'my_method_api': function (request, response) {
    var fields = Object.create(null)
    request.form.on('field', function (name, value) {
      fields[name] = value
    })
    request.form.on('file', function (name, file) {
      fields[name] = fs.readFileSync(file.path, 'utf8')
    })
    request.form.once('end', function () {
      // do something with your data
    })
  }
})

httpAuth

The httpauth middleware acts as an auth precondition, checking the authorization headers sent with the request.

If the request doesn't pass the authorization check, httpauth will close the response using the standard JSONTransport:

response.serveJSON(null, {
  httpStatusCode: 401,
  headers: { 'www-authenticate': 'Basic realm=\'' + realm + '\'' }
})

This will trigger a user/password prompt in your browser

Syntax

ApiServer.httpauth([options])

Options

Example

var apiserver = new ApiServer()
apiserver.use(/1\/admin\/.+/, ApiServer.httpauth({
  realm: 'signin please',
  credentials: ['foo:password','bar:password', ...],
  encode: true // we suppose that at the other end of the wire we have a browser
}))
apiserver.addModule('1', 'admin', {
  'protectedApi': function (request, response) {
    // this will executed only if you provide valid credentials
  }
})

How to contribute

ApiServer follows the awesome Vincent Driessen branching model.

ApiServer follows (more or less) the Felix's Node.js Style Guide, your contribution must be consistent with this style.

The test suite is written on top of visionmedia/mocha and it took hours of hard work. Please use the tests to check if your contribution is breaking some part of the library and add new tests for each new feature.

⚡ npm test

and for your test coverage

⚡ make test-cov

License

This software is released under the MIT license cited below.

Copyright (c) 2010 Kilian Ciuffolo, me@nailik.org. All Rights Reserved.

Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the 'Software'), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.