RESTful Resource API

Now that we have a created persistent storage of our Project resources, we are ready to start building the API that will share those resources with our single-page application.

This sharing will be accomplished through a RESTful web API. As we laid out in the routing discussion, a RESTful resource uses a well-defined url pattern to carry out CRUD actions (create, delete, update, and destroy). In fact, we'll use much the same pattern as we did for that lesson, with one important difference. Instead of serving HTML, we'll serve JSON.

The Projects Module

Let's start by defining a resource module to represent our Projects:

"use strict"; /** @module project * A RESTful resource representing a software project * implementing the CRUD methods. */ module.exports = { list: list, create: create, read: read, update: update, destroy: destroy }

We'll need to define each of the CRUD methods we want to use.

List

This action provides a list of all projects in our database. Since Sqlite3 provides us with an array of JavaScript objects representing our rows, we can simply use JSON.stringify() on that result to create the JSON we want to serve:

/** @function list * Sends a list of all projects as a JSON array. * @param {http.incomingRequest} req - the request object * @param {http.serverResponse} res - the response object * @param {sqlite3.Database} db - the database object */ function list(req, res, db) { db.all("SELECT * FROM projects", [], function(err, projects){ if(err) { console.error(err); res.statusCode = 500; res.end("Server Error") } res.setHeader("Content-Type", "text/json"); res.end(JSON.stringify(projects)); }); }

Create

The create action gets a bit more involved, as we must receive POSTed data. This data could come in various forms - as URL-encoded form data, as multipart form data, or we could even accept JSON or custom data types we've created for ourselves.

We could keep it simple and assume that we'll always receive just one type of data... after all, it'll be us creating the requests on the client side through AJAX. But let's experiment with how we can handle multiple types of incoming data.

var multipart = require('../lib/form-multipart'); var urlencoded = require('../lib/form-urlencoded'); var json = require('../lib/form-json'); /** @function create * Creates a new project and adds it to the database. * @param {http.incomingRequest} req - the request object * @param {http.serverResponse} res - the response object * @param {sqlite3.Database} db - the database object */ function create(req, res, db) { // A helper function to carry out the database // insertion once the POST data has been processed function insert(req, res, db) { var project = req.body; db.run("INSERT INTO projects (name, description, version, repository, license) VALUES (?,?,?,?,?)", [project.name, project.description, project.version, project.repository, project.license], function(err) { if(err) { console.error(err); res.statusCode = 500; res.end("Could not insert project into database"); return; } res.statusCode = 200; res.end(); } ); } // Determine what kind of data we're dealing with switch(req.headers['content-type']) { case 'multipart/form-data': // A multipart form multipart(req, res, function(req, res){ insert(req, res, db); }); break; case 'application/x-www-form-urlencoded': // Standard form encoding urlencoded(req, res, function(req, res){ insert(req, res, db); }); break; case 'application/json': case 'text/json': // A JSON string\ json(req, res, function(req, res) { insert(req, res, db); }); break; } }

Here we've used our multipart middleware from the file upload lesson to parse the body of multipart requests, and once that parsing has finished, we use the insert() helper function to save the project to our database. The urlencoded and json middleware echo the multipart functionality, but with url-encoded and JSON encoded data, respectfully. These are both much simpler to handle, as can be seen in form-encoded.js:

/** * @module form-urlencoded * A module for processing urlencoded HTTP requests */ module.exports = urlencoded; var querystring = require('querystring'); /** * @function urlencoded * Takes a request and response object, * parses the body of the url-encoded request * and attaches its contents to the request * object. If an error occurs, we log it * and send a 500 status code. Otherwise * we invoke the next callback with the * request and response. * @param {http.incomingRequest} req the request object * @param {http.serverResponse} res the repsonse object * @param {function} next the next function in the req/res pipeline */ function urlencoded(req, res, next) { var body = ""; // Handle error events by logging the error // and responding with a 500 server error req.on('error', function(err){ console.log(err); res.statusCode = 500; res.statusMessage = "Server error"; res.end("Server err"); }); // Handle data events by appending the new // data to the chunks array. req.on('data', function(chunk) { body += chunk; }); // Handle end events by parsing the body and // attaching the resulting object to the request object. req.on('end', function() { // Store the parsed body in the req.body property req.body = querystring.parse(body); // trigger the next callback with the modified req object next(req, res); } }

and in form-json.js:

/** * @module form-json * A module for processing JSON encoded HTTP requests */ module.exports = json; /** * @function json * Takes a request and response object, * parses the body of the JSON encoded request * and attaches its contents to the request * object. If an error occurs, we log it * and send a 500 status code. Otherwise * we invoke the next callback with the * request and response. * @param {http.incomingRequest} req the request object * @param {http.serverResponse} res the repsonse object * @param {function} next the next function in the req/res pipeline */ function urlencoded(req, res, next) { var body = ""; // Handle error events by logging the error // and responding with a 500 server error req.on('error', function(err){ console.log(err); res.statusCode = 500; res.statusMessage = "Server error"; res.end("Server err"); }); // Handle data events by appending the new // data to the chunks array. req.on('data', function(chunk) { body += chunk; }); // Handle end events by parsing the body and // attaching the resulting object to the request object. req.on('end', function() { // Store the parsed body in the req.body property req.body = JSON.parse(body); // trigger the next callback with the modified req object next(req, res); } }

Read

Read is back to being a simple retrieval from the database:

/** @function read * Serves a specific project as a JSON string * @param {http.incomingRequest} req - the request object * @param {http.serverResponse} res - the response object * @param {sqlite3.Database} db - the database object */ function read(req, res, db) { var id = req.params.id; db.get("SELECT * FROM projects WHERE id=?", [id], function(err, project){ if(err) { console.error(err); res.statusCode = 500; res.end("Server error"); return; } if(!project) { res.statusCode = 404; res.end("Project not found"); return; } res.setHeader("Content-Type", "text/json"); res.end(JSON.stringify(project)); }); }

Update

Whereas with update, we once again must handle POSTed data:

/** @update * Updates a specific record with the supplied values * @param {http.incomingRequest} req - the request object * @param {http.serverResponse} res - the response object * @param {sqlite3.Database} db - the database object */ function update(req, res, db) { var id = req.params.id; // Helper function to update the database record function update(req, res, db) { var project = req.body; db.run("UPDATE projects SET name=?, description=?, version=?, repository=?, license=? WHERE id=?", [project.name, project.description, project.version, project.repository, project.license, id], function(err) { if(err) { console.error(err); res.statusCode = 500; res.end("Could not update project in database"); return; } res.statusCode = 200; res.end(); } ); } // Determine what kind of data we're dealing with switch(req.headers['content-type']) { case 'multipart/form-data': // A multipart form multipart(req, res, function(req, res){ insert(req, res, db); }); break; case 'application/x-www-form-urlencoded': // Standard form encoding urlencoded(req, res, function(req, res){ insert(req, res, db); }); break; case 'application/json': case 'text/json': // A JSON string\ json(req, res, function(req, res) { insert(req, res, db); }); break; } }

Destroy

Destroy is also a relatively simple function:

/** @destroy * Removes the specified project from the database. * @param {http.incomingRequest} req - the request object * @param {http.serverResponse} res - the response object * @param {sqlite3.Database} db - the database object */ function destroy(req, res, db) { var id = req.params.id; db.run("DELETE FROM projects WHERE id=?", [id], function(err) { if(err) { console.error(err); res.statusCode = 500; res.end("Server error"); } res.statusCode = 200; res.end(); }); }

RESTful Routes

Now that we have a CRUD-implmenting resource, we need to define our resource routes, much like we did in the routing discussion. We'll want to use the route module we defined there to establish the routes in oure server.js file:

"use strict"; var PORT = 3000; var http = require('http'); var fileserver = require('./lib/fileserver'); var sqlite3 = require('sqlite3').verbose(); var db = new sqlite3.Database('scrumtastic.sqlite3', function(err) { if(err) console.error(err); }); var router = new (require('./lib/route')).Router(db); // Cache static directory in the fileserver fileserver.loadDir('public'); // Define our routes var project = require('./src/resource/project'); router.resource('/projects', project); var server = new http.Server(function(req, res) { // Remove the leading '/' from the resource url var resource = req.url.slice(1); // If no resource is requested, serve the cached index page. if(resource == '') fileserver.serveFile('public/index.html', req, res); // If the resource is cached in the fileserver, serve it else if(fileserver.isCached(resource)) fileserver.serveFile(resource, req, res); // Otherwise, route the request else router.route(req, res); }); // Launch the server server.listen(PORT, function(){ console.log("listening on port " + PORT); });

Testing the API

The router automatically creates routes for each of the CRUD methods defined on our Project resource. We can test this by CURLing our api:

>curl localhost:3000/projects

Curl is a unix tool that may not be available on windows. An alternative would be entering the address into a browser. In both cases, the result should be JSON text. But if we don't have any projects created, the text will be an empty array, i.e. [].

We can go ahead and seed the database using a new migration, 2-seed-projects.sql:

INSERT INTO projects (name, description, version, repository, license) VALUES ("Testy", "A Test Project", "v 0.0", null, "MIT");

Once the file is added to your migrations directory, you can re-run your migrations with npm run migrate. Now if you curl, or visit the page, you should see a project named "Testy".