Quick Start
Setting up a server is quick and easy. Here is a 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’s plugins include 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.plugins.pre.userAgentConnection());
Sinatra style handler chains
Like many other Node.js based REST frameworks, restify leverages a Sinatra style syntax for defining routes and the function handlers that service those routes:
server.get('/', function(req, res, next) {
res.send('home')
return next();
});
server.post('/foo',
function(req, res, next) {
req.someData = 'foo';
return next();
},
function(req, res, next) {
res.send(req.someData);
return next();
}
);
In a restify server, there are three distinct handler chains:
pre
- a handler chain executed prior to routinguse
- a handler chain executed post routing{httpVerb}
- a handler chain executed specific to a route
All three handler chains accept either a single function, multiple functions, or an array of functions.
Universal pre-handlers: server.pre()
The pre
handler chain is executed before routing. That means these handlers
will execute for an incoming request even if it’s for a route that you did not
register. This can be useful for logging metrics or for cleaning up the
incoming request before routing it.
// dedupe slashes in URL before routing
server.pre(restify.plugins.pre.dedupeSlashes());
Universal handlers: server.use()
The use
handler chains is executed after a route has been chosen to service
the request. Function handlers that are attached via the use()
method will be
run for all routes. Since restify runs handlers in the order they are
registered, make sure that all your use()
calls happen before defining any
routes.
server.use(function(req, res, next) {
console.warn('run for all routes!');
return next();
});
Using next()
Upon completion of each function in the handler chain, you are responsible for
calling next()
. Calling next()
will move to the next function in the chain.
Unlike other REST frameworks, calling res.send()
does not trigger next()
automatically. In many applications, work can continue to happen after
res.send()
, so flushing the response is not synonymous with completion of a
request.
In the normal case, next()
does not typically take any parameters. If for
some reason you want to stop processing the request, you can call next(false)
to stop processing the request:
server.use([
function(req, res, next) {
if (someCondition) {
res.send('done!');
return next(false);
}
return next();
},
function(req, res, next) {
// if someCondition is true, this handler is never executed
}
]);
next()
also accepts any object for which instanceof Error
is true, which
will cause restify to send that Error object as a response to the client. The
status code for the response will be inferred from the Error object’s
statusCode
property. If no statusCode
is found, it will default to 500.
So the snippet below will send a serialized error to the client with an http
500:
server.use(function(req, res, next) {
return next(new Error('boom!'));
});
And this will send a 404, since the NotFoundError
constructor provides a
value of 404 for statusCode
:
server.use(function(req, res, next) {
return next(new NotFoundError('not here!'));
});
Calling res.send()
with an Error object produces similar results, with this
snippet sending an http 500 with a serialized error the client:
server.use(function(req, res, next) {
res.send(new Error('boom!'));
return next();
});
The difference between the two is that invoking next()
with an Error object
allows you to leverage the server’s event
emitter. This enables you to handle all
occurrences of an error type using a common handler. See the error
handling section for more details.
Lastly, you can call next.ifError(err)
with an Error object to cause restify
to throw, bringing down the process. This can be useful if you an Error is
surfaced that cannot be handled, requiring you to kill the process.
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
. Those 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
.
Routes can be specified by any of the following http verbs - del
, get
,
head
, opts
, post
, put
, and patch
.
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();
}
server.get('/hello/:name', restify.plugins.conditionalHandler([
{ version: '1.1.3', handler: sendV1 },
{ version: '2.0.0', handler: 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": "~3 is not supported by GET /hello/mark"
}
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 response a response from the version 1 handler function, but then we asked
for V2 and got back JSON. Finally, we asked for a version that doesn’t exist
and got an error.
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('/hello/:name', restify.plugins.conditionalHandler([
{ version: ['2.0.0', '2.1.0', '2.2.0'], handler: sendV2 }
]));
In this case you may need to know more information such as what the original requested version string was, and what the matching version from the routes supported version array was. Two methods make this info available:
server.get('/version/test', restify.plugins.conditionalHandler([
{
version: ['2.0.0', '2.1.0', '2.2.0'],
handler: function (req, res, next) {
res.send(200, {
requestedVersion: req.version(),
matchedVersion: req.matchedVersion()
});
return next();
}
}
]));
Hitting this route will respond as below:
$ curl -s -H 'accept-version: <2.2.0' localhost:8080/version/test | json
{
"requestedVersion": "<2.2.0",
"matchedVersion": "2.1.0"
}
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);
});
Responses’ Content Negotiation And Formatting
If you’re using res.send()
restify will determine the content-type to respond
with by, from highest priority to lowest priority:
- using the value of
res.contentType
if present - otherwise, using the value of the
Content-Type
response header if set - otherwise, using
application/json
if the body is an object that is not a Buffer instance - otherwise, negotiating the content-type by matching available formatters with
the request’s
accept
header
If a content-type can’t be determined, then restify will respond with an error.
If a content-type can be negotiated, restify then determines what formatter to use to format the response’s content.
If no formatter matching the content-type can be found, restify will by default
override the response’s content-type to 'application/octet-stream'
and then
error if no formatter is found for that content-type.
This default behavior can be changed by passing strictFormatters: false
(default is false) when creating the restify server instance. In that case, if
no formatter is found for the negotiated content-type, the response is flushed
without applying any formatter.
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);
}
}
});
For example, attempting to send a content-type that does not have a defined formatter:
server.get('/foo', function(req, res, next) {
res.setHeader('content-type', 'text/css');
res.send('hi');
return next();
});
Will result in a response with a content-type of application/octet-stream
:
$ curl -i localhost:3000/
HTTP/1.1 200 OK
Content-Type: application/octet-stream
Content-Length: 2
Date: Thu, 02 Jun 2016 06:50:54 GMT
Connection: keep-alive
However, if the server instance is created with strictFormatters:false
:
var server = restify.createServer({
strictFormatters: false
});
The response would has a content-type of text/css
even though no 'text/css'
formatter is present:
$ curl -i localhost:3000/
HTTP/1.1 200 OK
Content-Type: text/css
Content-Length: 2
Date: Thu, 02 Jun 2016 06:50:54 GMT
Connection: keep-alive
As previously noted, restify ships with built-in formatters for 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);
}
}
});
Restify ships with the following default formatters, which can be overridden
when passing a formatters options to createServer()
:
- application/javascript
- application/json
- text/plain
- application/octet-stream
The restify response object retains 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
It is common to want to handle an error conditions 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 the InternalServer
error event 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. You can also use a generic restifyError
event
which will catch errors of all types.
An example of sending a 404:
server.get('/hello/:foo', function(req, res, next) {
// resource not found error
var err = new restify.errors.NotFoundError('oh noes!');
return next(err);
});
server.on('NotFound', function (req, res, err, cb) {
// do not call res.send! you are now in an error context and are outside
// of the normal next chain. you can log or do metrics here, and invoke
// the callback when you're done. restify will automtically render the
// NotFoundError depending on the content-type header you have set in your
// response.
return cb();
});
For customizing the error being sent back to the client:
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('InternalServer', function (req, res, err, cb) {
// by default, restify will usually render the Error object as plaintext or
// JSON depending on content negotiation. the default text formatter and JSON
// formatter are pretty simple, they just call toString() and toJSON() on the
// object being passed to res.send, which in this case, is the error object.
// so to customize what it sent back to the client when this error occurs,
// you would implement as follows:
// for any response that is text/plain
err.toString = function toString() {
return 'an internal server error occurred!';
};
// for any response that is application/json
err.toJSON = function toJSON() {
return {
message: 'an internal server error occurred!',
code: 'boom!'
}
};
return cb();
});
server.on('restifyError', function (req, res, err, cb) {
// this listener will fire after both events above!
// `err` here is the same as the error that was passed to the above
// error handlers.
return cb();
});
Here is another example of InternalServerError
, but this time with a custom
formatter:
const errs = require('restify-errors');
const server = restify.createServer({
formatters: {
'text/html': function(req, res, body) {
if (body instanceof Error) {
// body here is an instance of InternalServerError
return '<html><body>' + body.message + '</body></html>';
}
}
}
});
server.get('/', function(req, res, next) {
res.header('content-type', 'text/html');
return next(new errs.InternalServerError('oh noes!'));
});
restify-errors
A module called restify-errors exposes a suite of error constructors for many
common http and REST related errors. These constructors can be used in
conjunction with the next(err)
pattern to easily leverage the server’s event
emitter. The full list of constructors can be viewed over at the
restify-errors repository. Here are some
examples:
var errs = require('restify-errors');
server.get('/', function(req, res, next) {
return next(new errs.ConflictError("I just don't like you"));
});
$ curl -is localhost:3000
HTTP/1.1 409 Conflict
Content-Type: application/json
Content-Length: 53
Date: Fri, 03 Jun 2016 20:29:45 GMT
Connection: keep-alive
{"code":"Conflict","message":"I just don't like you"}
When using restify-errors, you can also directly call res.send(err)
, and
restify will automatically serialize your error for you:
var errs = require('restify-errors');
server.get('/', function(req, res, next) {
res.send(new errs.GoneError('gone girl'));
return next();
});
$ curl -is localhost:8080/
HTTP/1.1 410 Gone
Content-Type: application/json
Content-Length: 37
Date: Fri, 03 Jun 2016 20:17:48 GMT
Connection: keep-alive
{"code":"Gone","message":"gone girl"}
This automatic serialization happens because the JSON formatter will call
JSON.stringify()
on the Error object, and all restify-errors have a toJSON
method defined. Compare this to a standard Error object which does not have
toJSON
defined:
server.get('/sendErr', function(req, res, next) {
res.send(new Error('where is my msg?'));
return next();
});
server.get('/nextErr', function(req, res, next) {
return next(new Error('where is my msg?'));
});
$ curl -is localhost:8080/sendErr
HTTP/1.1 410 Gone
Content-Type: application/json
Content-Length: 37
Date: Fri, 03 Jun 2016 20:17:48 GMT
Connection: keep-alive
{}
$ curl -is localhost:8080/nextErr
HTTP/1.1 410 Gone
Content-Type: application/json
Content-Length: 37
Date: Fri, 03 Jun 2016 20:17:48 GMT
Connection: keep-alive
{}
If you want to use custom errors, make sure you have toJSON
defined, or use
restify-error’s makeConstructor()
method to automatically create Errors that
are supported with with toJSON
.
HttpError
restify-errors provides constructors that inherit from either HttpError or
RestError. All HttpErrors have a numeric http statusCode
and body
properties. 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
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 errs = require('restify-errors');
var server = restify.createServer();
server.get('/hello/:name', function(req, res, next) {
return next(new errs.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 HttpErrors are:
- BadRequestError (400 Bad Request)
- UnauthorizedError (401 Unauthorized)
- PaymentRequiredError (402 Payment Required)
- ForbiddenError (403 Forbidden)
- NotFoundError (404 Not Found)
- MethodNotAllowedError (405 Method Not Allowed)
- NotAcceptableError (406 Not Acceptable)
- ProxyAuthenticationRequiredError (407 Proxy Authentication Required)
- RequestTimeoutError (408 Request Time-out)
- ConflictError (409 Conflict)
- GoneError (410 Gone)
- LengthRequiredError (411 Length Required)
- PreconditionFailedError (412 Precondition Failed)
- RequestEntityTooLargeError (413 Request Entity Too Large)
- RequesturiTooLargeError (414 Request-URI Too Large)
- UnsupportedMediaTypeError (415 Unsupported Media Type)
- RequestedRangeNotSatisfiableError (416 Requested Range Not Satisfiable)
- ExpectationFailedError (417 Expectation Failed)
- ImATeapotError (418 I’m a teapot)
- UnprocessableEntityError (422 Unprocessable Entity)
- LockedError (423 Locked)
- FailedDependencyError (424 Failed Dependency)
- UnorderedCollectionError (425 Unordered Collection)
- UpgradeRequiredError (426 Upgrade Required)
- PreconditionRequiredError (428 Precondition Required)
- TooManyRequestsError (429 Too Many Requests)
- RequestHeaderFieldsTooLargeError (431 Request Header Fields Too Large)
- InternalServerError (500 Internal Server Error)
- NotImplementedError (501 Not Implemented)
- BadGatewayError (502 Bad Gateway)
- ServiceUnavailableError (503 Service Unavailable)
- GatewayTimeoutError (504 Gateway Time-out)
- HttpVersionNotSupportedError (505 HTTP Version Not Supported)
- VariantAlsoNegotiatesError (506 Variant Also Negotiates)
- InsufficientStorageError (507 Insufficient Storage)
- BandwidthLimitExceededError (509 Bandwidth Limit Exceeded)
- NotExtendedError (510 Not Extended)
- NetworkAuthenticationRequiredError (511 Network Authentication Required)
- BadDigestError (400 Bad Request)
- BadMethodError (405 Method Not Allowed)
- InternalError (500 Internal Server Error)
- InvalidArgumentError (409 Conflict)
- InvalidContentError (400 Bad Request)
- InvalidCredentialsError (401 Unauthorized)
- InvalidHeaderError (400 Bad Request)
- InvalidVersionError (400 Bad Request)
- MissingParameterError (409 Conflict)
- NotAuthorizedError (403 Forbidden)
- RequestExpiredError (400 Bad Request)
- RequestThrottledError (429 Too Many Requests)
- ResourceNotFoundError (404 Not Found)
- WrongAcceptError (406 Not Acceptable)
And the built in RestErrors are:
- 400 BadDigestError
- 405 BadMethodError
- 500 InternalError
- 409 InvalidArgumentError
- 400 InvalidContentError
- 401 InvalidCredentialsError
- 400 InvalidHeaderError
- 400 InvalidVersionError
- 409 MissingParameterError
- 403 NotAuthorizedError
- 412 PreconditionFailedError
- 400 RequestExpiredError
- 429 RequestThrottledError
- 404 ResourceNotFoundError
- 406 WrongAcceptError
You can also create your own subclasses using the makeConstructor
method:
var errs = require('restify-errors');
var restify = require('restify');
errs.makeConstructor('ZombieApocalypseError');
var myErr = new errs.ZombieApocalypseError('zomg!');
The constructor takes message
, statusCode
, restCode
, and context
options. Please check out the restify-errors repo for more information.
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);
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);
});
- Previous
- Next