About restify

restify is a node.js module built specifically to enable you to build correct REST web services. It intentionally borrows heavily from express as that is more or less the de facto API for writing web applications on top of node.js.

Why use restify and not express?

I get asked this more than anything else, so I'll just get it out of the way up front.

Express' use case is targeted at browser applications and contains a lot of functionality, such as templating and rendering, to support that. Restify does not.

Restify exists to let you build "strict" API services that are maintanable and observable. Restify comes with automatic DTrace support for all your handlers, if you're running on a platform that supports DTrace.

In short, I wrote restify as I needed a framework that gave me absolute control over interactions with HTTP and full observability into the latency and characteristics of my applications. If you don't need that, or don't care about those aspect(s), then it's probably not for you.

For real time chat, discussion and support, join the #restify IRC channel on irc.freenode.net.

About this guide

This guide provides comprehensive documentation on writing a REST api (server) with restify, writing clients that easily consume REST APIs, and on the DTrace integration present in restify.

Note this documentation refers to the 2.x version(s) of restify; these versions are not backwards-compatible with the 0.x and 1.x versions.

If you're migrating from an earlier version of restify, see 1.4 to 2.0 Migration Tips

Conventions

Any content formatted like this:

curl localhost:8080

is a command-line example that you can run from a shell. All other examples and information is formatted like this:

GET /foo HTTP/1.1

Installation

npm install restify

Server API

The most barebones echo server:

var restify = require('restify');

function respond(req, res, next) {
  res.send('hello ' + req.params.name);
  next();
}

var server = restify.createServer();
server.get('/hello/:name', respond);
server.head('/hello/:name', respond);

server.listen(8080, function() {
  console.log('%s listening at %s', server.name, server.url);
});

Try hitting that with the following curl commands to get a feel for what restify is going to turn that into:

curl -is http://localhost:8080/hello/mark -H 'accept: text/plain'
HTTP/1.1 200 OK
Content-Type: text/plain
Content-Length: 10
Date: Mon, 31 Dec 2012 01:32:44 GMT
Connection: keep-alive

hello mark


$ curl -is http://localhost:8080/hello/mark
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 12
Date: Mon, 31 Dec 2012 01:33:33 GMT
Connection: keep-alive

"hello mark"


$ curl -is http://localhost:8080/hello/mark -X HEAD -H 'connection: close'
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 12
Date: Mon, 31 Dec 2012 01:42:07 GMT
Connection: close

Note that by default, curl uses Connection: keep-alive. In order to make the HEAD method return right away, you'll need to pass Connection: close.

Since curl is often used with REST APIs, restify provides a plugin to work around this idiosyncrasy in curl. The plugin checks whether the user agent is curl. If it is, it sets the Connection header to "close" and removes the "Content-Length" header.

server.pre(restify.pre.userAgentConnection());

See the pre method for more information.

Creating a Server

Creating a server is straightforward, as you simply invoke the createServer API, which takes an options object with the options listed below (and listen() takes the same arguments as node's http.Server.listen):

var restify = require('restify');

var server = restify.createServer({
  certificate: ...,
  key: ...,
  name: 'MyApp',
});

server.listen(8080);
OptionTypeDescription
certificateStringIf you want to create an HTTPS server, pass in the PEM-encoded certificate and key
keyStringIf you want to create an HTTPS server, pass in the PEM-encoded certificate and key
formattersObjectCustom response formatters for res.send()
logObjectYou can optionally pass in a bunyan instance; not required
nameStringBy default, this will be set in the Server response header, default is restify
spdyObjectAny options accepted by node-spdy
versionStringA default version to set for all routes
handleUpgradesBooleanHook the upgrade event from the node HTTP server, pushing Connection: Upgrade requests through the regular request handling chain; defaults to false
httpsServerOptionsObjectAny options accepted by node-https Server. If provided the following restify server options will be ignored: spdy, ca, certificate, key, passphrase, rejectUnauthorized, requestCert and ciphers; however these can all be specified on httpsServerOptions.

Common handlers: server.use()

A restify server has a use() method that takes handlers of the form function (req, res, next). Note that restify runs handlers in the order they are registered on a server, so if you want some common handlers to run before any of your routes, issue calls to use() before defining routes.

Note that in all calls to use() and the routes below, you can pass in any combination of direct functions (function(res, res, next)) and arrays of functions ([function(req, res, next)]).

Routing

restify routing, in 'basic' mode, is pretty much identical to express/sinatra, in that HTTP verbs are used with a parameterized resource to determine what chain of handlers to run. Values associated with named placeholders are available in req.params. Note that values will be URL-decoded before being passed to you.

 function send(req, res, next) {
   res.send('hello ' + req.params.name);
   return next();
 }

 server.post('/hello', function create(req, res, next) {
   res.send(201, Math.random().toString(36).substr(3, 8));
   return next();
 });
 server.put('/hello', send);
 server.get('/hello/:name', send);
 server.head('/hello/:name', send);
 server.del('hello/:name', function rm(req, res, next) {
   res.send(204);
   return next();
 });

You can also pass in a RegExp object and access the capture group with req.params (which will not be interpreted in any way):

server.get(/^\/([a-zA-Z0-9_\.~-]+)\/(.*)/, function(req, res, next) {
  console.log(req.params[0]);
  console.log(req.params[1]);
  res.send(200);
  return next();
});

Here any request like:

curl localhost:8080/foo/my/cats/name/is/gandalf

Would result in req.params[0] being foo and req.params[1] being my/cats/name/is/gandalf. Basically, you can do whatever you want.

Note the use of next(). You are responsible for calling next() in order to run the next handler in the chain. As below, you can pass an Error object in to have restify automatically return responses to the client.

You can pass in false to not error, but to stop the handler chain. This is useful if you had a res.send in an early filter, which is not an error, and you possibly have one later you want to short-circuit.

Lastly, you can pass in a string name to next(), and restify will lookup that route, and assuming it exists will run the chain from where you left off. So for example:

var count = 0;

server.use(function foo(req, res, next) {
    count++;
    next();
});

server.get('/foo/:id', function (req, res, next) {
   next('foo2');
});

server.get({
    name: 'foo2',
    path: '/foo/:id'
}, function (req, res, next) {
   assert.equal(count, 1);
   res.send(200);
   next();
});

Note that foo only gets run once in that example. A few caveats:

Chaining Handlers

Routes can also accept more than one handler function. For instance:

server.get(
    '/foo/:id',
    function(req, res, next) {
        console.log('Authenticate');
        return next();
    },
    function(req, res, next) {
        res.send(200);
        return next();
    }
);

Hypermedia

If a parameterized route was defined with a string (not a regex), you can render it from other places in the server. This is useful to have HTTP responses that link to other resources, without having to hardcode URLs throughout the codebase. Both path and query strings parameters get URL encoded appropriately.

server.get({name: 'city', path: '/cities/:slug'}, /* ... */);

// in another route
res.send({
  country: 'Australia',
  // render a URL by specifying the route name and parameters
  capital: server.router.render('city', {slug: 'canberra'}, {details: true})
});

Which returns:

{
  "country": "Australia",
  "capital": "/cities/canberra?details=true"
}

Versioned Routes

Most REST APIs tend to need versioning, and restify ships with support for semver versioning in an Accept-Version header, the same way you specify NPM version dependencies:

var restify = require('restify');

var server = restify.createServer();

function sendV1(req, res, next) {
  res.send('hello: ' + req.params.name);
  return next();
}

function sendV2(req, res, next) {
  res.send({hello: req.params.name});
  return next();
}

var PATH = '/hello/:name';
server.get({path: PATH, version: '1.1.3'}, sendV1);
server.get({path: PATH, version: '2.0.0'}, sendV2);

server.listen(8080);

Try hitting with:

curl -s localhost:8080/hello/mark
"hello: mark"
$ curl -s -H 'accept-version: ~1' localhost:8080/hello/mark
"hello: mark"
$ curl -s -H 'accept-version: ~2' localhost:8080/hello/mark
{"hello":"mark"}
$ curl -s -H 'accept-version: ~3' localhost:8080/hello/mark | json
{
  "code": "InvalidVersion",
  "message": "GET /hello/mark supports versions: 1.1.3, 2.0.0"
}

In the first case, we didn't specify an Accept-Version header at all, so restify treats that like sending a *. Much as not sending an Accept header means the client gets the server's choice. Restify will choose this highest matching route. In the second case, we explicitly asked for for V1, which got us the same response, but then we asked for V2 and got back JSON. Finally, we asked for a version that doesn't exist and got an error (notably, we didn't send an Accept header, so we got a JSON response). Which segues us nicely into content negotiation.

You can default the versions on routes by passing in a version field at server creation time. Lastly, you can support multiple versions in the API by using an array:

  server.get({path: PATH, version: ['2.0.0', '2.1.0']}, sendV2);

Upgrade Requests

Incoming HTTP requests that contain a Connection: Upgrade header are treated somewhat differently by the node HTTP server. If you want restify to push Upgrade requests through the regular routing chain, you need to enable handleUpgrades when creating the server.

To determine if a request is eligible for Upgrade, check for the existence of res.claimUpgrade(). This method will return an object with two properties: the socket of the underlying connection, and the first received data Buffer as head (may be zero-length).

Once res.claimUpgrade() is called, res itself is marked unusable for further HTTP responses; any later attempt to send() or end(), etc, will throw an Error. Likewise if res has already been used to send at least part of a response to the client, res.claimUpgrade() will throw an Error. Upgrades and regular HTTP Response behaviour are mutually exclusive on any particular connection.

Using the Upgrade mechanism, you can use a library like watershed to negotiate WebSockets connections. For example:

var ws = new Watershed();
server.get('/websocket/attach', function upgradeRoute(req, res, next) {
  if (!res.claimUpgrade) {
    next(new Error('Connection Must Upgrade For WebSockets'));
    return;
  }

  var upgrade = res.claimUpgrade();
  var shed = ws.accept(req, upgrade.socket, upgrade.head);
  shed.on('text', function(msg) {
    console.log('Received message from websocket client: ' + msg);
  });
  shed.send('hello there!');

  next(false);
});

Content Negotiation

If you're using res.send() restify will automatically select the content-type to respond with, by finding the first registered formatter defined. Note in the examples above we've not defined any formatters, so we've been leveraging the fact that restify ships with application/json, text/plain and application/octet-stream formatters. You can add additional formatters to restify by passing in a hash of content-type -> parser at server creation time:

var server = restify.createServer({
  formatters: {
    'application/foo': function formatFoo(req, res, body) {
      if (body instanceof Error)
        return body.stack;

      if (Buffer.isBuffer(body))
        return body.toString('base64');

      return util.inspect(body);
    }
  }
});

You can do whatever you want, but you probably want to check the type of body to figure out what type it is, notably for Error/Buffer/everything else. You can always add more formatters later by just setting the formatter on server.formatters, but it's probably sane to just do it at construct time. Also, note that if a content-type can't be negotiated, the default is application/octet-stream. Of course, you can always explicitly set the content-type:

res.setHeader('content-type', 'application/foo');
res.send({hello: 'world'});

Note that there are typically at least three content-types supported by restify: json, text and binary. When you override or append to this, the "priority" might change; to ensure that the priority is set to what you want, you should set a q-value on your formatter definitions, which will ensure sorting happens the way you want:

restify.createServer({
  formatters: {
    'application/foo; q=0.9': function formatFoo(req, res, body) {
      if (body instanceof Error)
        return body.stack;

      if (Buffer.isBuffer(body))
        return body.toString('base64');

      return util.inspect(body);
    }
  }
});

Lastly, you don't have to use any of this magic, as a restify response object has all the "raw" methods of a node ServerResponse on it as well.

var body = 'hello world';
res.writeHead(200, {
  'Content-Length': Buffer.byteLength(body),
  'Content-Type': 'text/plain'
});
res.write(body);
res.end();

Error handling

You can handle errors in restify a few different ways. First, you can always just call res.send(err). You can also shorthand this in a route by doing:

server.get('/hello/:name', function(req, res, next) {
  return database.get(req.params.name, function(err, user) {
    if (err)
      return next(err);

    res.send(user);
    return next();
  });
});

If you invoke res.send() with an error that has a statusCode attribute, that will be used, otherwise a default of 500 will be used (unless you're using res.send(4xx, new Error('blah))).

Alternatively, restify 2.1 supports a next.ifError API:

server.get('/hello/:name', function(req, res, next) {
  return database.get(req.params.name, function(err, user) {
    next.ifError(err);
    res.send(user);
    next();
  });
});

Sometimes, for all requests, you may want to handle an error condition the same way. As an example, you may want to serve a 500 page on all InternalServerErrors. In this case you can add a listener for this error that is always fired when this Error is encountered by Restify as part of a next(error) statement. This gives you a way to handle all errors of the same class identically across the server.

server.get('/hello/:name', function(req, res, next) {
  // some internal unrecoverable error
  var err = new restify.errors.InternalServerError('oh noes!');
  return next(err);
});

server.on('InternalServerError', function (req, res, err, cb) {
  err._customContent = 'something is wrong!';
  return cb();
});

HttpError

Now the obvious question is what that exactly does (in either case). restify tries to be programmer-friendly with errors by exposing all HTTP status codes as a subclass of HttpError. So, for example, you can do this:

server.get('/hello/:name', function(req, res, next) {
  return next(new restify.ConflictError("I just don't like you"));
});

$ curl -is -H 'accept: text/*' localhost:8080/hello/mark
HTTP/1.1 409 Conflict
Content-Type: text/plain
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET
Access-Control-Allow-Headers: Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, Api-Version
Access-Control-Expose-Headers: Api-Version, Request-Id, Response-Time
Connection: close
Content-Length: 21
Content-MD5: up6uNh2ejV/C6JUbLlvsiw==
Date: Tue, 03 Jan 2012 00:24:48 GMT
Server: restify
Request-Id: 1685313e-e801-4d90-9537-7ca20a27acfc
Response-Time: 1

I just don't like you

Alternatively, you can access the error classes via restify.errors. We can do this with a simple change to the previous example:

server.get('/hello/:name', function(req, res, next) {
  return next(new restify.errors.ConflictError("I just don't like you"));
});

The core thing to note about an HttpError is that it has a numeric code (statusCode) and a body. The statusCode will automatically set the HTTP response status code, and the body attribute by default will be the message.

All status codes between 400 and 5xx are automatically converted into an HttpError with the name being 'PascalCase' and spaces removed. For the complete list, take a look at the node source.

From that code above 418: I'm a teapot would be ImATeapotError, as an example.

RestError

Now, a common problem with REST APIs and HTTP is that they often end up needing to overload 400 and 409 to mean a bunch of different things. There's no real standard on what to do in these cases, but in general you want machines to be able to (safely) parse these things out, and so restify defines a convention of a RestError. A RestError is a subclass of one of the particular HttpError types, and additionally sets the body attribute to be a JS object with the attributes code and message. For example, here's a built-in RestError:

var server = restify.createServer();
server.get('/hello/:name', function(req, res, next) {
  return next(new restify.InvalidArgumentError("I just don't like you"));
});

$ curl -is localhost:8080/hello/mark | json
HTTP/1.1 409 Conflict
Content-Type: application/json
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET
Access-Control-Allow-Headers: Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, Api-Version
Access-Control-Expose-Headers: Api-Version, Request-Id, Response-Time
Connection: close
Content-Length: 60
Content-MD5: MpEcO5EQFUZ2MNeUB2VaZg==
Date: Tue, 03 Jan 2012 00:50:21 GMT
Server: restify
Request-Id: bda456dd-2fe4-478d-809c-7d159d58d579
Response-Time: 3

{
  "code": "InvalidArgument",
  "message": "I just don't like you"
}

The built-in restify errors are:

You can always add your own by subclassing restify.RestError like:

var restify = require('restify');
var util = require('util');

function MyError(message) {
  restify.RestError.call(this, {
    restCode: 'MyError',
    statusCode: 418,
    message: message,
    constructorOpt: MyError
  });
  this.name = 'MyError';
};
util.inherits(MyError, restify.RestError);

Basically, a RestError takes a statusCode, a restCode, a message, and a "constructorOpt" so that V8 correctly omits your code from the stack trace (you don't have to do that, but you probably want it). In the example above, we also set the name property so console.log(new MyError()); looks correct.

Socket.IO

To use socket.io with restify, just treat your restify server as if it were a "raw" node server:

var server = restify.createServer();
var io = socketio.listen(server);

server.get('/', function indexHTML(req, res, next) {
    fs.readFile(__dirname + '/index.html', function (err, data) {
        if (err) {
            next(err);
            return;
        }

        res.setHeader('Content-Type', 'text/html');
        res.writeHead(200);
        res.end(data);
        next();
    });
});


io.sockets.on('connection', function (socket) {
    socket.emit('news', { hello: 'world' });
    socket.on('my other event', function (data) {
            console.log(data);
    });
});

server.listen(8080, function () {
    console.log('socket.io server listening at %s', server.url);
});

Server API

Events

Restify servers emit all the events from the node http.Server and has several other events you want to listen on.

Event: 'NotFound'

function (request, response, error, cb) {}

When a client request is sent for a URL that does not exist, restify will emit this event. Note that restify checks for listeners on this event, and if there are none, responds with a default 404 handler. It is expected that if you listen for this event, you respond to the client.

Event: 'MethodNotAllowed'

function (request, response, error, cb) {}

When a client request is sent for a URL that does exist, but you have not registered a route for that HTTP verb, restify will emit this event. Note that restify checks for listeners on this event, and if there are none, responds with a default 405 handler. It is expected that if you listen for this event, you respond to the client.

Event: 'VersionNotAllowed'

function (request, response, error, cb) {}

When a client request is sent for a route that exists, but does not match the version(s) on those routes, restify will emit this event. Note that restify checks for listeners on this event, and if there are none, responds with a default 400 handler. It is expected that if you listen for this event, you respond to the client.

Event: UnsupportedMediaType'

function (request, response, error, cb) {}

When a client request is sent for a route that exist, but has a content-type mismatch, restify will emit this event. Note that restify checks for listeners on this event, and if there are none, responds with a default 415 handler. It is expected that if you listen for this event, you respond to the client.

Event: 'after'

function (request, response, route, error) {}

Emitted after a route has finished all the handlers you registered. You can use this to write audit logs, etc. The route parameter will be the Route object that ran. Note that when you are using the default 404/405/BadVersion handlers, this event will still be fired, but route will be null. If you have registered your own listeners for those, this event will not be fired unless you invoke the cb argument that is provided with them.

Event: 'uncaughtException'

function (request, response, route, error) {}

Emitted when some handler throws an uncaughtException somewhere in the chain. The default behavior is to just call res.send(error), and let the built-ins in restify handle transforming, but you can override to whatever you want here.

Properties

A restify server has the following properties on it:

NameTypeDescription
nameStringname of the server
versionStringdefault version to use in all routes
logObjectbunyan instance
acceptableArray(String)list of content-types this server can respond with
urlStringOnce listen() is called, this will be filled in with where the server is running

Other Methods

address()

Wraps node's address().

listen(port, [host], [callback]) or listen(path, [callback])

Wraps node's listen().

close()

Wraps node's close().

pre()

Allows you to add in handlers that run before routing occurs. This gives you a hook to change request headers and the like if you need to. Note that req.params will be undefined, as that's filled in after routing.

server.pre(function(req, res, next) {
  req.headers.accept = 'application/json';  // screw you client!
  return next();
});

You can also clean up URLs before routes are matched with the built in restify.pre.sanitizePath. This re-implements v1.4 behaviour

server.pre(restify.pre.sanitizePath());

use()

Allows you to add in handlers that run no matter what the route.

Bundled Plugins

restify ships with several handlers you can use, specifically:

Here's some example code using all the shipped plugins:

var server = restify.createServer();
server.use(restify.acceptParser(server.acceptable));
server.use(restify.authorizationParser());
server.use(restify.dateParser());
server.use(restify.queryParser());
server.use(restify.jsonp());
server.use(restify.gzipResponse());
server.use(restify.bodyParser());
server.use(restify.throttle({
  burst: 100,
  rate: 50,
  ip: true,
  overrides: {
    '192.168.1.1': {
      rate: 0,        // unlimited
      burst: 0
    }
  }
}));
server.use(restify.conditionalRequest());

Accept Parser

Parses out the Accept header, and ensures that the server can respond to what the client asked for. You almost always want to just pass in server.acceptable here, as that's an array of content types the server knows how to respond to (with the formatters you've registered). If the request is for a non-handled type, this plugin will return an error of 406.

server.use(restify.acceptParser(server.acceptable));

Authorization Parser

server.use(restify.authorizationParser());

Parses out the Authorization header as best restify can. Currently only HTTP Basic Auth and HTTP Signature schemes are supported. When this is used, req.authorization will be set to something like:

{
  scheme: <Basic|Signature|...>,
  credentials: <Undecoded value of header>,
  basic: {
    username: $user
    password: $password
  }
}

req.username will also be set, and defaults to 'anonymous'. If the scheme is unrecognized, the only thing available in req.authorization will be scheme and credentials - it will be up to you to parse out the rest.

CORS

server.use(restify.CORS());

Supports tacking CORS headers into actual requests (as defined by the spec). Note that preflight requests are automatically handled by the router, and you can override the default behavior on a per-URL basis with server.opts(:url, ...). To fully specify this plugin, a sample invocation is:

server.use(restify.CORS({
    origins: ['https://foo.com', 'http://bar.com', 'http://baz.com:8081'],   // defaults to ['*']
    credentials: true,                 // defaults to false
    headers: ['x-foo']                 // sets expose-headers
}));

Date Parser

server.use(restify.dateParser());

Parses out the HTTP Date header (if present) and checks for clock skew (default allowed clock skew is 300s, like Kerberos). You can pass in a number, which is interpreted in seconds, to allow for clock skew.

// Allows clock skew of 1m
server.use(restify.dateParser(60));

QueryParser

server.use(restify.queryParser());

Parses the HTTP query string (i.e., /foo?id=bar&name=mark). If you use this, the parsed content will always be available in req.query, additionally params are merged into req.params. You can disable by passing in mapParams: false in the options object:

server.use(restify.queryParser({ mapParams: false }));

JSONP

Supports checking the query string for callback or jsonp and ensuring that the content-type is appropriately set if JSONP params are in place. There is also a default application/javascript formatter to handle this.

You should set the queryParser plugin to run before this, but if you don't this plugin will still parse the query string properly.

BodyParser

Blocks your chain on reading and parsing the HTTP request body. Switches on Content-Type and does the appropriate logic. application/json, application/x-www-form-urlencoded and multipart/form-data are currently supported.

server.use(restify.bodyParser({
    maxBodySize: 0,
    mapParams: true,
    mapFiles: false,
    overrideParams: false,
    multipartHandler: function(part) {
        part.on('data', function(data) {
          /* do something with the multipart data */
        });
    },
    multipartFileHandler: function(part) {
        part.on('data', function(data) {
          /* do something with the multipart file data */
        });
    },
    keepExtensions: false,
    uploadDir: os.tmpdir(),
    multiples: true
 }));

Options:

RequestLogger

Sets up a child bunyan logger with the current request id filled in, along with any other parameters you define.

server.use(restify.requestLogger({
    properties: {
        foo: 'bar'
    },
    serializers: {...}
}));

You can pass in no options to this, in which case only the request id will be appended, and no serializers appended (this is also the most performant); the logger created at server creation time will be used as the parent logger. This logger can be used normally, with req.log.

This plugin does _not_ log each individual request. Use the Audit Logging plugin or a custom middleware for that use.

GzipResponse

server.use(restify.gzipResponse());

If the client sends an accept-encoding: gzip header (or one with an appropriate q-val), then the server will automatically gzip all response data. Note that only gzip is supported, as this is most widely supported by clients in the wild. This plugin will overwrite some of the internal streams, so any calls to res.send, res.write, etc., will be compressed. A side effect is that the content-length header cannot be known, and so transfer-encoding: chunked will always be set when this is in effect. This plugin has no impact if the client does not send accept-encoding: gzip.

Serve Static

The serveStatic module is different than most of the other plugins, in that it is expected that you are going to map it to a route, as below:

server.get(/\/docs\/current\/?.*/, restify.serveStatic({
  directory: './documentation/v1',
  default: 'index.html'
}));

The above route and directory combination will serve a file located in ./documentation/v1/docs/current/index.html when you attempt to hit http://localhost:8080/docs/current/.

The plugin will enforce that all files under directory are served. The directory served is relative to the process working directory. You can also provide a default parameter such as index.html for any directory that lacks a direct file match. You can specify additional restrictions by passing in a match parameter, which is just a RegExp to check against the requested file name. Lastly, you can pass in a maxAge numeric, which will set the Cache-Control header. Default is 3600 (1 hour).

Throttle

restify ships with a fairly comprehensive implementation of Token bucket, with the ability to throttle on IP (or x-forwarded-for) and username (from req.username). You define "global" request rate and burst rate, and you can define overrides for specific keys. Note that you can always place this on per-URL routes to enable different request rates to different resources (if for example, one route, like /my/slow/database is much easier to overwhlem than /my/fast/memcache).

server.use(restify.throttle({
  burst: 100,
  rate: 50,
  ip: true,
  overrides: {
    '192.168.1.1': {
      rate: 0,        // unlimited
      burst: 0
    }
  }
}));

If a client has consumed all of their available rate/burst, an HTTP response code of 429 Too Many Requests is returned.

Options:

NameTypeDescription
rateNumberSteady state number of requests/second to allow
burstNumberIf available, the amount of requests to burst to
ipBooleanDo throttling on a /32 (source IP)
xffBooleanDo throttling on a /32 (X-Forwarded-For)
usernameBooleanDo throttling on req.username
overridesObjectPer "key" overrides
tokensTableObjectStorage engine; must support put/get
maxKeysNumberIf using the built-in storage table, the maximum distinct throttling keys to allow at a time

Note that ip, xff and username are XOR'd.

Using an external storage mechanism for key/bucket mappings.

By default, the restify throttling plugin uses an in-memory LRU to store mappings between throttling keys (i.e., IP address) to the actual bucket that key is consuming. If this suits you, you can tune the maximum number of keys to store in memory with options.maxKeys; the default is 10000.

In some circumstances, you want to offload this into a shared system, such as Redis, if you have a fleet of API servers and you're not getting steady and/or uniform request distribution. To enable this, you can pass in options.tokensTable, which is simply any Object that supports put and get with a String key, and an Object value.

Conditional Request Handler

server.use(restify.conditionalRequest());

You can use this handler to let clients do nice HTTP semantics with the "match" headers. Specifically, with this plugin in place, you would set res.etag=$yourhashhere, and then this plugin will do one of:

The specific headers this plugin looks at are:

Some example usage:

server.use(function setETag(req, res, next) {
  res.header('ETag', 'myETag');
  res.header('Last-Modified', new Date());
});

server.use(restify.conditionalRequest());

server.get('/hello/:name', function(req, res, next) {
  res.send('hello ' + req.params.name);
});

Audit Logging

Audit logging is a special plugin, as you don't use it with .use(), but with the after event:

server.on('after', restify.auditLogger({
  log: bunyan.createLogger({
    name: 'audit',
    stream: process.stdout
  })
}));

You pass in the auditor a bunyan logger, and it will write out records at the info level. Records will look like this:

    {
      "name": "audit",
      "hostname": "your.host.name",
      "audit": true,
      "remoteAddress": "127.0.0.1",
      "remotePort": 57692,
      "req_id": "ed634c3e-1af0-40e4-ad1e-68c2fb67c8e1",
      "req": {
        "method": "GET",
        "url": "/foo",
        "headers": {
          "authorization": "Basic YWRtaW46am95cGFzczEyMw==",
          "user-agent": "curl/7.19.7 (universal-apple-darwin10.0) libcurl/7.19.7 OpenSSL/0.9.8r zlib/1.2.3",
          "host": "localhost:8080",
          "accept": "application/json"
        },
        "httpVersion": "1.1",
        "trailers": {},
        "version": "*",
        "timers": {
          "bunyan": 52,
          "saveAction": 8,
          "reqResTracker": 213,
          "addContext": 8,
          "addModels": 4,
          "resNamespaces": 5,
          "parseQueryString": 11,
          "instanceHeaders": 20,
          "xForwardedProto": 7,
          "httpsRedirector": 14,
          "readBody": 21,
          "parseBody": 6,
          "xframe": 7,
          "restifyCookieParser": 15,
          "fooHandler": 23,
          "barHandler": 14,
          "carHandler": 14
        }
      },
      "res": {
        "statusCode": 200,
        "headers": {
          "access-control-allow-origin": "*",
          "access-control-allow-headers": "Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, Api-Version",
          "access-control-expose-headers": "Api-Version, Request-Id, Response-Time",
          "server": "Joyent SmartDataCenter 7.0.0",
          "x-request-id": "ed634c3e-1af0-40e4-ad1e-68c2fb67c8e1",
          "access-control-allow-methods": "GET",
          "x-api-version": "1.0.0",
          "connection": "close",
          "content-length": 158,
          "content-md5": "zkiRn2/k3saflPhxXI7aXA==",
          "content-type": "application/json",
          "date": "Tue, 07 Feb 2012 20:30:31 GMT",
          "x-response-time": 1639
        },
        "trailer": false
      },
      "route": {
      "name": "GetFoo",
      "version": ["1.0.0"]
      },
      "secure": false,
      "level": 30,
      "msg": GetFoo handled: 200",
      "time": "2012-02-07T20:30:31.896Z",
      "v": 0
    }

The timers field shows the time each handler took to run in microseconds. Restify by default will record this information for every handler for each route. However, if you decide to include nested handlers, you can track the timing yourself by utilizing the Request startHandlerTimer and endHandlerTimer API.

Request API

Wraps all of the node http.IncomingMessage APIs, events and properties, plus the following.

header(key, [defaultValue])

Get the case-insensitive request header key, and optionally provide a default value (express-compliant):

req.header('Host');
req.header('HOST');
req.header('Accept', '*/*');

accepts(type)

(express-compliant)

Check if the Accept header is present, and includes the given type.

When the Accept header is not present true is returned. Otherwise the given type is matched by an exact match, and then subtypes. You may pass the subtype such as html which is then converted internally to text/html using the mime lookup table.

// Accept: text/html
req.accepts('html');
// => true

// Accept: text/*; application/json
req.accepts('html');
req.accepts('text/html');
req.accepts('text/plain');
req.accepts('application/json');
// => true

req.accepts('image/png');
req.accepts('png');
// => false

is(type)

Check if the incoming request contains the Content-Type header field, and it contains the give mime type.

// With Content-Type: text/html; charset=utf-8
req.is('html');
req.is('text/html');
// => true

// When Content-Type is application/json
req.is('json');
req.is('application/json');
// => true

req.is('html');
// => false

Note this is almost compliant with express, but restify does not have all the app.is() callback business express does.

isSecure()

Check if the incoming request is encrypted.

isChunked()

Check if the incoming request is chunked.

isKeepAlive()

Check if the incoming request is kept alive.

log

Note that you can piggyback on the restify logging framework, by just using req.log. I.e.,:

function myHandler(req, res, next) {
  var log = req.log;

  log.debug({params: req.params}, 'Hello there %s', 'foo');
}

The advantage to doing this is that each restify req instance has a new bunyan instance log on it where the request id is automatically injected in, so you can easily correlate your high-throughput logs together.

getLogger(component)

Shorthand to grab a new bunyan instance that is a child component of the one restify has:

var log = req.getLogger('MyFoo');

time()

The time when this request arrived (ms since epoch)

startHandlerTimer(handlerName)

Start the timer for a request handler. You might want to use this if you've got a Restify request handler in your chain that contains nested handlers.

function fooHandler(req, res, next) {
    vasync.pipeline(funcs: [
        function nestedHandler1(req, res, next) {
            req.startHandlerTimer('nestedHandler1');
            // do something
            req.endHandlerTimer('nestedHandler1');
            return next();
        },
        function nestedHandler1(req, res, next) {
            req.startHandlerTimer('nestedHandler2');
            // do something
            req.endHandlerTimer('nestedHandler2');
            return next();

        }...
    ]...
}

endHandlerTimer(handlerName)

End the timer for a request handler. You must invoke this function if you called startRequestHandler on a handler. Otherwise the time recorded will be incorrect.

Properties

NameTypeDescription
contentLengthNumbershort hand for the header content-length
contentTypeStringshort hand for the header content-type
hrefStringurl.parse(req.url) href
logObjectbunyan logger you can piggyback on
idStringA unique request id (x-request-id)
pathStringcleaned up URL path

Response API

Wraps all of the node ServerResponse APIs, events and properties, plus the following.

header(key, value)

Get or set the response header key.

res.header('Content-Length');
// => undefined

res.header('Content-Length', 123);
// => 123

res.header('Content-Length');
// => 123

res.header('foo', new Date());
// => Fri, 03 Feb 2012 20:09:58 GMT

charSet(type)

Appends the provided character set to the response's Content-Type.

res.charSet('utf-8');

Will change the normal json Content-Type to application/json; charset=utf-8.

cache([type], [options])

Sets the cache-control header. type defaults to _public_, and options currently only takes maxAge.

res.cache();

status(code)

Sets the response statusCode.

res.status(201);

send([status], body)

You can use send() to wrap up all the usual writeHead(), write(), end() calls on the HTTP API of node. You can pass send either a code and body, or just a body. body can be an Object, a Buffer, or an Error. When you call send(), restify figures out how to format the response (see content-negotiation, above), and does that.

res.send({hello: 'world'});
res.send(201, {hello: 'world'});
res.send(new BadRequestError('meh'));

json([status], body)

Short-hand for:

res.contentType = 'json';
res.send({hello: 'world'});

Properties

NameTypeDescription
codeNumberHTTP status code
contentLengthNumbershort hand for the header content-length
contentTypeStringshort hand for the header content-type
headersObjectresponse headers
idStringA unique request id (x-request-id)

Setting the default headers

You can change what headers restify sends by default by setting the top-level property defaultResponseHeaders. This should be a function that takes one argument data, which is the already serialized response body. data can be either a String or Buffer (or null). The this object will be the response itself.

var restify = require('restify');

restify.defaultResponseHeaders = function(data) {
  this.header('Server', 'helloworld');
};

restify.defaultResponseHeaders = false; // disable altogether

DTrace

One of the coolest features of restify is that it automatically creates DTrace probes for you whenever you add a new route/handler. The easiest way to explain this is with an example:

var restify = require('restify');

var server = restify.createServer({
  name: 'helloworld'
});

server.use(restify.acceptParser(server.acceptable));
server.use(restify.authorizationParser());
server.use(restify.dateParser());
server.use(restify.queryParser());
server.use(restify.urlEncodedBodyParser());

server.use(function slowHandler(req, res, next) {
  setTimeout(function() {
    return next();
  }, 250);
});

server.get({path: '/hello/:name', name: 'GetFoo'}, function respond(req, res, next) {
  res.send({
    hello: req.params.name
  });
  return next();
});

server.listen(8080, function() {
  console.log('listening: %s', server.url);
});

So we've got our typical "hello world" server now, with a slight twist; we introduced an artificial 250ms lag. Also, note that we named our server, our routes, and all of our handlers (functions); while that's optional, it does make DTrace much more usable. So, if you started that server, then looked for DTrace probes, you'd see something like this:

dtrace -l -P restify*
ID   PROVIDER            MODULE                          FUNCTION NAME
24 restify38789       mod-88f3f88                       route-start route-start
25 restify38789       mod-88f3f88                     handler-start handler-start
26 restify38789       mod-88f3f88                      handler-done handler-done
27 restify38789       mod-88f3f88                        route-done route-done

route-start

FieldTypeDescription
server namechar *name of the restify server that fired
route namechar *name of the route that fired
idintunique id for this request
methodchar *HTTP request method
urlchar *(full) HTTP URL
headerschar *JSON encoded map of all request headers

handler-start

FieldTypeDescription
server namechar *name of the restify server that fired
route namechar *name of the route that fired
handler namechar *name of the function that just entered
idintunique id for this request

route-done

FieldTypeDescription
server namechar *name of the restify server that fired
route namechar *name of the route that fired
idintunique id for this request
statusCodeintHTTP response code
headerschar *JSON encoded map of response headers

handler-done

FieldTypeDescription
server namechar *name of the restify server that fired
route namechar *name of the route that fired
handler namechar *name of the function that just entered
idintunique id for this request

Example D Script

Now, if you wanted to say get a breakdown of latency by handler, you could do something like this:

 #!/usr/sbin/dtrace -s
 #pragma D option quiet

 restify*:::route-start
 {
    track[arg2] = timestamp;
 }

 restify*:::handler-start
 /track[arg3]/
 {
    h[arg3, copyinstr(arg2)] = timestamp;
 }

 restify*:::handler-done
 /track[arg3] && h[arg3, copyinstr(arg2)]/
 {
    @[copyinstr(arg2)] = quantize((timestamp - h[arg3, copyinstr(arg2)]) / 1000000);
    h[arg3, copyinstr(arg2)] = 0;
 }

 restify*:::route-done
 /track[arg2]/
 {
    @[copyinstr(arg1)] = quantize((timestamp - track[arg2]) / 1000000);
    track[arg2] = 0;
 }

So running the server in one terminal:

node helloworld.js

The D script in another:

./helloworld.d

Hit the server a few times with curl:

for i in {1..10} ; do curl -is http://127.0.0.1:8080/hello/mark ; done

Then Ctrl-C the D script, and you'll see the "slowHandler" at the bottom of the stack, bucketized that it's the vast majority of latency in this pipeline

handler-6
value  ------------- Distribution ------------- count
-1 |                                         0
0 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 10
1 |                                         0

parseAccept
value  ------------- Distribution ------------- count
-1 |                                         0
0 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 10
1 |                                         0

parseAuthorization
value  ------------- Distribution ------------- count
-1 |                                         0
0 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 10
1 |                                         0

parseDate
value  ------------- Distribution ------------- count
-1 |                                         0
0 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 10
1 |                                         0

parseQueryString
value  ------------- Distribution ------------- count
-1 |                                         0
0 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 10
1 |                                         0

parseUrlEncodedBody
value  ------------- Distribution ------------- count
-1 |                                         0
0 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 10
1 |                                         0

respond
value  ------------- Distribution ------------- count
1 |                                         0
2 |@@@@                                     1
4 |                                         0
8 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@     9
16 |                                         0

slowHandler
value  ------------- Distribution ------------- count
64 |                                         0
128 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@     9
256 |@@@@                                     1
512 |                                         0

getfoo
value  ------------- Distribution ------------- count
64 |                                         0
128 |@@@@                                     1
256 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@     9
512 |

Client API

There are actually three separate clients shipped in restify:

The idea being that if you want to support "typical" control-plane REST APIs, you probably want the JsonClient, or if you're using some other serialization (like XML) you'd write your own client that extends the StringClient. If you need streaming support, you'll need to do some work on top of the HttpClient, as StringClient and friends buffer requests/responses.

All clients support retry with exponential backoff for getting a TCP connection; they do not perform retries on 5xx error codes like previous versions of the restify client. You can set retry to false to disable this logic altogether. Also, all clients support a connectTimeout field, which is use on each retry. The default is not to set a connectTimeout, so you end up with the node.js socket defaults.

Here's an example of hitting the Joyent CloudAPI:

var restify = require('restify');

// Creates a JSON client
var client = restify.createJsonClient({
  url: 'https://us-west-1.api.joyentcloud.com'
});


client.basicAuth('$login', '$password');
client.get('/my/machines', function(err, req, res, obj) {
  assert.ifError(err);

  console.log(JSON.stringify(obj, null, 2));
});

As a short-hand, a client can be initialized with a string-URL rather than an options object:

var restify = require('restify');

var client = restify.createJsonClient('https://us-west-1.api.joyentcloud.com');

Note that all further documentation refers to the "short-hand" form of methods like get/put/del which take a string path. You can also pass in an object to any of those methods with extra params (notably headers):

var options = {
  path: '/foo/bar',
  headers: {
    'x-foo': 'bar'
  },
  retry: {
    'retries': 0
  },
  agent: false
};

client.get(options, function(err, req, res) { .. });

If you need to interpose additional headers in the request before it is sent on to the server, you can provide a synchronous callback function as the signRequest option when creating a client. This is particularly useful with node-http-signature, which needs to attach a cryptographic signature of selected outgoing headers. If provided, this callback will be invoked with a single parameter: the outgoing http.ClientRequest object.

JsonClient

The JSON Client is the highest-level client bundled with restify; it exports a set of methods that map directly to HTTP verbs. All callbacks look like function(err, req, res, [obj]), where obj is optional, depending on if content was returned. HTTP status codes are not interpreted, so if the server returned 4xx or something with a JSON payload, obj will be the JSON payload. err however will be set if the server returned a status code >= 400 (it will be one of the restify HTTP errors). If obj looks like a RestError:

{
  "code": "FooError",
  "message": "some foo happened"
}

then err gets "upconverted" into a RestError for you. Otherwise it will be an HttpError.

createJsonClient(options)

var client = restify.createJsonClient({
  url: 'https://api.us-west-1.joyentcloud.com',
  version: '*'
});

Options:

NameTypeDescription
acceptStringAccept header to send
connectTimeoutNumberAmount of time to wait for a socket
requestTimeoutNumberAmount of time to wait for the request to finish
dtraceObjectnode-dtrace-provider handle
gzipObjectWill compress data when sent using content-encoding: gzip
headersObjectHTTP headers to set in all requests
logObjectbunyan instance
retryObjectoptions to provide to node-retry;"false" disables retry; defaults to 4 retries
signRequestFunctionsynchronous callback for interposing headers before request is sent
urlStringFully-qualified URL to connect to
userAgentStringuser-agent string to use; restify inserts one, but you can override it
versionStringsemver string to set the accept-version

get(path, callback)

Performs an HTTP get; if no payload was returned, obj defaults to {} for you (so you don't get a bunch of null pointer errors).

client.get('/foo/bar', function(err, req, res, obj) {
  assert.ifError(err);
  console.log('%j', obj);
});

head(path, callback)

Just like get, but without obj:

client.head('/foo/bar', function(err, req, res) {
  assert.ifError(err);
  console.log('%d -> %j', res.statusCode, res.headers);
});

post(path, object, callback)

Takes a complete object to serialize and send to the server.

client.post('/foo', { hello: 'world' }, function(err, req, res, obj) {
  assert.ifError(err);
  console.log('%d -> %j', res.statusCode, res.headers);
  console.log('%j', obj);
});

put(path, object, callback)

Just like post:

client.put('/foo', { hello: 'world' }, function(err, req, res, obj) {
  assert.ifError(err);
  console.log('%d -> %j', res.statusCode, res.headers);
  console.log('%j', obj);
});

del(path, callback)

del doesn't take content, since you know, it should't:

client.del('/foo/bar', function(err, req, res) {
  assert.ifError(err);
  console.log('%d -> %j', res.statusCode, res.headers);
});

StringClient

StringClient is what JsonClient is built on, and provides a base for you to write other buffering/parsing clients (like say an XML client). If you need to talk to some "raw" HTTP server, then StringClient is what you want, as it by default will provide you with content uploads in application/x-www-form-url-encoded and downloads as text/plain. To extend a StringClient, take a look at the source for JsonClient. Effectively, you extend it, and set the appropriate options in the constructor and implement a write (for put/post) and parse method (for all HTTP bodies), and that's it.

createStringClient(options)

var client = restify.createStringClient({
  url: 'https://example.com'
})

get(path, callback)

Performs an HTTP get; if no payload was returned, data defaults to '' for you (so you don't get a bunch of null pointer errors).

client.get('/foo/bar', function(err, req, res, data) {
  assert.ifError(err);
  console.log('%s', data);
});

head(path, callback)

Just like get, but without data:

client.head('/foo/bar', function(err, req, res) {
  assert.ifError(err);
  console.log('%d -> %j', res.statusCode, res.headers);
});

post(path, object, callback)

Takes a complete object to serialize and send to the server.

client.post('/foo', { hello: 'world' }, function(err, req, res, data) {
  assert.ifError(err);
  console.log('%d -> %j', res.statusCode, res.headers);
  console.log('%s', data);
});

put(path, object, callback)

Just like post:

client.put('/foo', { hello: 'world' }, function(err, req, res, data) {
  assert.ifError(err);
  console.log('%d -> %j', res.statusCode, res.headers);
  console.log('%s', data);
});

del(path, callback)

del doesn't take content, since you know, it should't:

client.del('/foo/bar', function(err, req, res) {
  assert.ifError(err);
  console.log('%d -> %j', res.statusCode, res.headers);
});

HttpClient

HttpClient is the lowest-level client shipped in restify, and is basically just some sugar over the top of node's http/https modules (with HTTP methods like the other clients). It is useful if you want to stream with restify. Note that the event below is unfortunately named result and not response (because Event 'response' is already used).

client = restify.createClient({
  url: 'http://127.0.0.1'
});

client.get('/str/mcavage', function(err, req) {
  assert.ifError(err); // connection error

  req.on('result', function(err, res) {
    assert.ifError(err); // HTTP status code >= 400

    res.body = '';
    res.setEncoding('utf8');
    res.on('data', function(chunk) {
      res.body += chunk;
    });

    res.on('end', function() {
      console.log(res.body);
    });
  });
});

Or a write:

client.post(opts, function(err, req) {
  assert.ifError(connectErr);

  req.on('result', function(err, res) {
    assert.ifError(err);
    res.body = '';
    res.setEncoding('utf8');
    res.on('data', function(chunk) {
      res.body += chunk;
    });

    res.on('end', function() {
      console.log(res.body);
    });
  });

  req.write('hello world');
  req.end();
});

Note that get/head/del all call req.end() for you, so you can't write data over those. Otherwise, all the same methods exist as JsonClient/StringClient.

One wishing to extend the HttpClient should look at the internals and note that read and write probably need to be overridden.

basicAuth(username, password)

Since it hasn't been mentioned yet, this convenience method (available on all clients), just sets the Authorization header for all HTTP requests:

client.basicAuth('mark', 'mysupersecretpassword');

Upgrades

If you successfully negotiate an Upgrade with the HTTP server, an upgradeResult event will be emitted with the arguments err, res, socket and head. You can use this functionality to establish a WebSockets connection with a server. For example, using the watershed library:

var ws = new Watershed();
var wskey = ws.generateKey();
var options = {
  path: '/websockets/attach',
  headers: {
    connection: 'upgrade',
    upgrade: 'websocket',
    'sec-websocket-key': wskey,
  }
};
client.get(options, function(err, res, socket, head) {
  req.once('upgradeResult', function(err, res, socket, head) {
    var shed = ws.connect(res, socket, head, wskey);
    shed.on('text', function(msg) {
      console.log('message from server: ' + msg);
      shed.end();
    });
    shed.send('greetings program');
  });
});