image of require restify code

Creating a Basic node.js API with Restify and Save

Posted 30/5/2013/ by Dom Udall

We've done a number of node.js projects which have required building a basic API; either for mobile applications to be able to access data, or for multiple client websites to access data. These have mostly been built using express.js and a mongo database , though recently I've been turned onto restify - a cutdown system specifically for building APIs. I've also started using save , an npm module created by Clock's Paul Serby to be persistence agnostic, allowing a number of different storage systems to be used.

In this post I'm going to run through how to build a very basic system, allowing for creation, updating, removing, listing, and getting entities. You'll need to have node and cURL installed, and know how to use a command line, other than that it should be fairly straight forward =)

Start by creating a new folder for the project:

mkdir my-api
cd my-api

Install the modules that are needed vianpm:

npm install restify save

save comes with a built-in memory engine for persistence, meaning it can be played with while the app is running without the use of a database.

Create an app.js file using your favourite editor, and require the modules in your first lines:

var restify = require('restify')
  , userSave = require('save')('user')

Then, you'll actually want to create your restify server, and get it listening on a port. This part will be familiar to those working with express:

var server = restify.createServer({ name: 'my-api' })
 
server.listen(3000, function () {
  console.log('%s listening at %s', server.name, server.url)
})

Now you can run your app and you'll be presented with some nice output:

my-api listening at http://0.0.0.0:3000

Boom! Now you're up and running, however your API is pretty useless; no requests at all will be processed as there are none defined. Before we get onto that, there are some plugins that need to be put into restify to solve a couple of issues:

server
  .use(restify.fullResponse())
  .use(restify.bodyParser())

The fullResponse plugin sets up all of the default headers for the system, we won't tinker with those within this walkthrough. The bodyParser remaps the body content of a request to the req.params variable, allowing both GET and POST/PUT routes to use the same interface, fairly handy when you want to re-use code.

Lets start with a simple route; getting all of the users in the system. None will exist, however save will give us an empty array as a response, enough to get started with.

server.get('/user', function (req, res, next) {
  userSave.find({}, function (error, users) {
    res.send(users)
  })
})

First line again will be familiar with express users. For those who aren't, HTTP verbs are used to determine what chain of handlers to run on a given route, in this case, a GET request on /user. The callback then defines the functionality we want; getting save to find users. Once that's completed, then all the users are sent back in a response. This response will set an HTTP status code of 200 by default, however you can send other status codes if you want, as you'll see later.

So lets test the new route and see if an empty array is returned. For this we'll use cURL, a command line tool used to transfer data and, in this case, send HTTP requests. Assuming you're running this on your local machine, and are using port 3000 like the example code above, run this command:

curl -i http://localhost:3000/user

This should return the empty array[], as well as a lot of response headers due to the -i option (includes protocol headers in the output). All well and good, but lets work out how to actually put a user into the system to make this call useful.

A slightly more complex route is required; posting new user data to a URL. Add this route to your app.js:

server.post('/user', function (req, res, next) {
  if (req.params.name === undefined) {
    return next(new restify.InvalidArgumentError('Name must be supplied'))
  }
 
  userSave.create({ name: req.params.name }, function (error, user) {
    if (error) return next(new restify.InvalidArgumentError(JSON.stringify(error.errors)))
 
    res.send(201, user)
  })
})

You'll see that we're using the same URL as the previous route, however we're using a different HTTP verb to change the behaviour of the API. The POST request requires data to be passed through; in this case just a name for the user. We're also doing some basic validation (extremely basic) by checking that the name is actually set. If not, we return an error defined by restify. The reason for using this error is so that the correct response header is used, and restify provides a useful response to any clients using the API. The same process is used when trying to create the new user via save; if there are any errors there, they get created in the correct form and passed to restify. If all ends well, then the user is created and returned in the response, along with the '201 Created' HTTP response code.

Once this is put in, you can POST a new user to the API using the following cURL example:

curl -i -X POST -d 'name=Dom' http://localhost:3000/user

As you'll see, we're changing the verb using the -X option, and passing through data in the -d option. You should get the following response:

{"name":"Dom","_id":"1"}

Awesome. Let's give the validation a test as well by not actually supplying a name:

curl -i -X POST http://localhost:3000/user

This should give a response of:

{"code":"InvalidArgument","message":"Name must be supplied"}

Of course, when building an actual system, you'll want a lot more validation and verification than this. The error handling for restify also doesn't provide a way of returning an array of errors, so a custom implementation needs to be put in place. We'll cover that in a different post.

Let's check that user is still in the system using the first route built. Note if you've restarted the app AFTER creating the user, they won't exist. Save uses memory persistence by default, so keep it running!

curl -i http://localhost:3000/user

Should return a lovely array of:

[{"name":"Dom","_id":"1"}]

Now we're getting somewhere! So what if we only want the details of this single user? Let's create a new GET route for this scenario. Again, if you've used express, you'll recognise that there is a named parameter (:id) in the route. This is used to map part of the URL to a variable within the request. This variable is then used to find a single item within save.

You don't need validation to check if the ID is being passed through, as the route checks a value exists by default. You can go one step further and use RegEx to validate the parameter to whatever extent you wish. Save may provide an error, or undefined for the user variable if they don't exist. This is covered by the basic if function, returning an empty response with a '404 Not Found' header if no user is found.

server.get('/user/:id', function (req, res, next) {
  userSave.findOne({ _id: req.params.id }, function (error, user) {
    if (error) return next(new restify.InvalidArgumentError(JSON.stringify(error.errors)))
 
    if (user) {
      res.send(user)
    } else {
      res.send(404)
    }
  })
})

If you've added the code and restarted the app, you'll need to create a user as above to be able to get them. Try getting a user before creating them:

curl -i http://localhost:3000/user/1

As expected, this will give you an empty response, and the correct 404 header. Once you've created them, try again, and you'll get the user object:

{"name":"Dom","_id":"1"}

Nice! Now the API can provide a list of all the users, create new ones, and return details of single users. Next, we're going to cover is editing users that already exist (we all make mistakes …).

This scenario introduces a new HTTP verb; PUT. There's a lot of online discussion on which should be used when, with my personal opinion being that POST is when you do not know where the resource will live (as with creating a user; we don't know their ID), and PUT for when the resources location is know. PUT in this scenario could be used with either an update or a create journey. For this tutorial, we'll be using just update.

As with the single user GET route, we're using the:id parameter to identify the user we wish to update. We're using the same validation for the update as with the create; making sure the user's name is passed through and not undefined. Once validated, the data get passed through to save's update, returning any errors if they happen, or a '200 OK' response header if all's well.

server.put('/user/:id', function (req, res, next) {
  if (req.params.name === undefined) {
    return next(new restify.InvalidArgumentError('Name must be supplied'))
  }
 
  userSave.update({ _id: req.params.id, name: req.params.name }, function (error, user) {
    if (error) return next(new restify.InvalidArgumentError(JSON.stringify(error.errors)))
    res.send()
  })
})

So getting back to cURL to check (again, you'll need to restart the app, so make sure you add a user before updating them), the request is similar to creating a user, but with a different path and a different verb:

curl -i -X PUT -d 'name=Joe' http://localhost:3000/user/1

Check the new user response using the single user route:

curl -i http://localhost:3000/user/1

Which should give you a response of:

{"name":"Joe","_id":"1"}

Now, for the final act, we'll get rid of a user using the DELETE HTTP verb.

Again, as with the single user GET route, we're using the :id parameter to identify the user we wish to delete. No validation is in place, as an error will occur if a user does not exist, as save will not be able to delete them. Other than that, the route just returns a '200 OK' if the user is successfully deleted.

server.del('/user/:id', function (req, res, next) {
  userSave.delete(req.params.id, function (error, user) {
    if (error) return next(new restify.InvalidArgumentError(JSON.stringify(error.errors)))
 
    res.send()
  })
})

Once more unto the breach, dear friends. Make a cURL request to create a user, and then destroy them:

curl -i -X DELETE http://localhost:3000/user/1

Again, you should get a 200 response with no content, indicating the user has been successfully deleted.

And that's about it for now! All the code for this walkthrough is available on git. There's a lot that I haven't covered here; schemas, validation, reusability, security… There's a pretty heafty list.

We're still learning about the development of APIs and how best to attack all the different facets. Please do comment, there will also be more posts coming looking into Hypermedia, documentation generation, security, and anything else we discover!

comments powered by Disqus
Back