Building a simple API with Express and Bookshelf.js

This article is a continuation from the last one - Using Node.js with MySQL

Featured in Node Weekly: Issue 67

It's been a long time since my last post, work commitments have kept me very busy - hopefully, next year I'll be able to publish more regularly.

As promised in the previous article, today we are going to be building a simple restful API with Bookshelf.js and Express. We need to install 2 additional modules for our application, namely, express - for routing, and body-parser - for parsing request variables:

  1. npm install express body-parser --save

I prefer defining all my main variables at the top of the file, let's go ahead and do that:

  1. var _ = require('lodash');
  2. var express = require('express');
  3. var app = express();
  4. var bodyParser = require('body-parser');
  5. // application routing
  6. var router = express.Router();
  7. // body-parser middleware for handling request variables
  8. app.use(bodyParser.urlencoded({extended: true}));
  9. app.use(bodyParser.json());

Right below the main variables we have also passed the body-parser middleware to be used by our application to handle request variables.

In the last article we ended by creating Models, it is also a good idea to create Collections which will give us the ability to perform wholesale operations on our Models.

Collections

  1. var Users = Bookshelf.Collection.extend({
  2. model: User
  3. });
  4. var Posts = Bookshelf.Collection.extend({
  5. model: Post
  6. });
  7. var Categories = Bookshelf.Collection.extend({
  8. model: Category
  9. });
  10. var Tags = Bookshelf.Collection.extend({
  11. model: Tag
  12. });

Next we need to define our API end points - we want to be able to perform basic CRUD operations on the following resources: users, categories, and posts.

Users

  • GET /users - fetch all users
  • POST /users - create a new user
  • GET /users/:id - fetch a single user by id
  • PUT /users/:id - update user
  • DELETE /users/:id - delete user

Categories

  • GET /categories - fetch all categories
  • POST /categories - create a new category
  • GET /categories/:id - fetch a single category
  • PUT /categories/:id - update category
  • DELETE /categories/:id - delete category

Posts

  • GET /posts - fetch all posts
  • POST /posts - create a new post
  • GET /posts/:id - fetch a single post by id
  • PUT /posts/:id - update post
  • DELETE /posts/:id - delete post
  • GET /posts/category/:id - fetch all posts from a single category
  • GET /posts/tags/:slug - fetch all posts from a single tag

All is set, now we can go ahead and start setting up our API routes. First up we'll create users routes, every post created will require a user_id.

  1. router.route('/users')
  2. // fetch all users
  3. .get(function (req, res) {
  4. Users.forge()
  5. .fetch()
  6. .then(function (collection) {
  7. res.json({error: false, data: collection.toJSON()});
  8. })
  9. .catch(function (err) {
  10. res.status(500).json({error: true, data: {message: err.message}});
  11. });
  12. })
  13. // create a user
  14. .post(function (req, res) {
  15. User.forge({
  16. name: req.body.name,
  17. email: req.body.email
  18. })
  19. .save()
  20. .then(function (user) {
  21. res.json({error: false, data: {id: user.get('id')}});
  22. })
  23. .catch(function (err) {
  24. res.status(500).json({error: true, data: {message: err.message}});
  25. });
  26. });
  27. router.route('/users/:id')
  28. // fetch user
  29. .get(function (req, res) {
  30. User.forge({id: req.params.id})
  31. .fetch()
  32. .then(function (user) {
  33. if (!user) {
  34. res.status(404).json({error: true, data: {}});
  35. }
  36. else {
  37. res.json({error: false, data: user.toJSON()});
  38. }
  39. })
  40. .catch(function (err) {
  41. res.status(500).json({error: true, data: {message: err.message}});
  42. });
  43. })
  44. // update user details
  45. .put(function (req, res) {
  46. User.forge({id: req.params.id})
  47. .fetch({require: true})
  48. .then(function (user) {
  49. user.save({
  50. name: req.body.name || user.get('name'),
  51. email: req.body.email || user.get('email')
  52. })
  53. .then(function () {
  54. res.json({error: false, data: {message: 'User details updated'}});
  55. })
  56. .catch(function (err) {
  57. res.status(500).json({error: true, data: {message: err.message}});
  58. });
  59. })
  60. .catch(function (err) {
  61. res.status(500).json({error: true, data: {message: err.message}});
  62. });
  63. })
  64. // delete a user
  65. .delete(function (req, res) {
  66. User.forge({id: req.params.id})
  67. .fetch({require: true})
  68. .then(function (user) {
  69. user.destroy()
  70. .then(function () {
  71. res.json({error: true, data: {message: 'User successfully deleted'}});
  72. })
  73. .catch(function (err) {
  74. res.status(500).json({error: true, data: {message: err.message}});
  75. });
  76. })
  77. .catch(function (err) {
  78. res.status(500).json({error: true, data: {message: err.message}});
  79. });
  80. });

The forge method is a simple helper function to instantiate a new Model without the need of using the new keyword.

Categories have a one-to-many relation with posts so it is also a good idea to define their routes next.

  1. router.route('/categories')
  2. // fetch all categories
  3. .get(function (req, res) {
  4. Categories.forge()
  5. .fetch()
  6. .then(function (collection) {
  7. res.json({error: false, data: collection.toJSON()});
  8. })
  9. .catch(function (err) {
  10. res.status(500).json({error: true, data: {message: err.message}});
  11. });
  12. })
  13. // create a new category
  14. .post(function (req, res) {
  15. Category.forge({name: req.body.name})
  16. .save()
  17. .then(function (category) {
  18. res.json({error: false, data: {id: category.get('id')}});
  19. })
  20. .catch(function (err) {
  21. res.status(500).json({error: true, data: {message: err.message}});
  22. });
  23. });
  24. router.route('/categories/:id')
  25. // fetch all categories
  26. .get(function (req, res) {
  27. Category.forge({id: req.params.id})
  28. .fetch()
  29. .then(function (category) {
  30. if(!category) {
  31. res.status(404).json({error: true, data: {}});
  32. }
  33. else {
  34. res.json({error: false, data: category.toJSON()});
  35. }
  36. })
  37. .catch(function (err) {
  38. res.status(500).json({error: true, data: {message: err.message}});
  39. });
  40. })
  41. // update a category
  42. .put(function (req, res) {
  43. Category.forge({id: req.params.id})
  44. .fetch({require: true})
  45. .then(function (category) {
  46. category.save({name: req.body.name || category.get('name')})
  47. .then(function () {
  48. res.json({error: false, data: {message: 'Category updated'}});
  49. })
  50. .catch(function (err) {
  51. res.status(500).json({error: true, data: {message: err.message}});
  52. });
  53. })
  54. .catch(function (err) {
  55. res.status(500).json({error: true, data: {message: err.message}});
  56. });
  57. })
  58. // delete a category
  59. .delete(function (req, res) {
  60. Category.forge({id: req.params.id})
  61. .fetch({require: true})
  62. .then(function (category) {
  63. category.destroy()
  64. .then(function () {
  65. res.json({error: true, data: {message: 'Category successfully deleted'}});
  66. })
  67. .catch(function (err) {
  68. res.status(500).json({error: true, data: {message: err.message}});
  69. });
  70. })
  71. .catch(function (err) {
  72. res.status(500).json({error: true, data: {message: err.message}});
  73. });
  74. });

The main purpose of this application is to provide an API for creating and reading blog posts, which brings us to the crux of this article - creating posts routes.

  1. router.route('/posts')
  2. // fetch all posts
  3. .get(function (req, res) {
  4. Posts.forge()
  5. .fetch()
  6. .then(function (collection) {
  7. res.json({error: false, data: collection.toJSON()});
  8. })
  9. .catch(function (err) {
  10. res.status(500).json({error: true, data: {message: err.message}});
  11. });
  12. });
  13. router.route('/posts/:id')
  14. // fetch a post by id
  15. .get(function (req, res) {
  16. Post.forge({id: req.params.id})
  17. .fetch({withRelated: ['category', 'tags']})
  18. .then(function (post) {
  19. if (!post) {
  20. res.status(404).json({error: true, data: {}});
  21. }
  22. else {
  23. res.json({error: false, data: post.toJSON()});
  24. }
  25. })
  26. .catch(function (err) {
  27. res.status(500).json({error: true, data: {message: err.message}});
  28. });
  29. });

The above GET routes provide the ability to fetch all posts or a single one.

Creating new posts is a bit complicated because we have to update 3 tables, the posts table, the tags table, and the posts_tags table. Here is what we are going to do - firstly, we'll collect all post variables and then parse the tags to create an array, secondly, save the post, thirdly, save the related tags, and lastly, attach the tags to the newly created post.

  1. router.route('/posts')
  2. .post(function (req, res) {
  3. var tags = req.body.tags;
  4. // parse tags variable
  5. if (tags) {
  6. tags = tags.split(',').map(function (tag){
  7. return tag.trim();
  8. });
  9. }
  10. else {
  11. tags = ['uncategorised'];
  12. }
  13. // save post variables
  14. Post.forge({
  15. user_id: req.body.user_id,
  16. category_id: req.body.category_id,
  17. title: req.body.title,
  18. slug: req.body.title.replace(/ /g, '-').toLowerCase(),
  19. html: req.body.post
  20. })
  21. .save()
  22. .then(function (post) {
  23. // post successfully saved
  24. // save tags
  25. saveTags(tags)
  26. .then(function (ids) {
  27. post.load(['tags'])
  28. .then(function (model) {
  29. // attach tags to post
  30. model.tags().attach(ids);
  31. res.json({error: false, data: {message: 'Tags saved'}});
  32. })
  33. .catch(function (err) {
  34. res.status(500).json({error: true, data: {message: err.message}});
  35. });
  36. })
  37. .catch(function (err) {
  38. res.status(500).json({error: true, data: {message: err.message}});
  39. });
  40. })
  41. .catch(function (err) {
  42. res.status(500).json({error: true, data: {message: err.message}});
  43. });
  44. });

The sole purpose of the saveTags function is saving tags - it accepts an array of tags when called and returns a promise which resolves with their table ids.

  1. function saveTags(tags) {
  2. // create tag objects
  3. var tagObjects = tags.map(function (tag) {
  4. return {
  5. name: tag,
  6. slug: tag.replace(/ /g, '-').toLowerCase()
  7. };
  8. });
  9. return Tags.forge()
  10. // fetch tags that already exist
  11. .query('whereIn', 'slug', _.pluck(tagObjects, 'slug'))
  12. .fetch()
  13. .then(function (existingTags) {
  14. var doNotExist = [];
  15. existingTags = existingTags.toJSON();
  16. // filter out existing tags
  17. if (existingTags.length > 0) {
  18. var existingSlugs = _.pluck(existingTags, 'slug');
  19. doNotExist = tagObjects.filter(function (t) {
  20. return existingSlugs.indexOf(t.slug) < 0;
  21. });
  22. }
  23. else {
  24. doNotExist = tagObjects;
  25. }
  26. // save tags that do not exist
  27. return new Tags(doNotExist).mapThen(function(model) {
  28. return model.save()
  29. .then(function() {
  30. return model.get('id');
  31. });
  32. })
  33. // return ids of all passed tags
  34. .then(function (ids) {
  35. return _.union(ids, _.pluck(existingTags, 'id'));
  36. });
  37. });
  38. }

This function uses a simple algorhythm:

  1. Before saving tags, check which ones already exist
  2. Filter the existing tags out and only save the new ones
  3. Get the ids of newly created tags and combine them with ids of existing ones
  4. Resolve the promise.

The hard part is done but we would also like to query our posts using categories and tags.

  1. router.route('/posts/category/:id')
  2. .get(function (req, res) {
  3. Category.forge({id: req.params.id})
  4. .fetch({withRelated: ['posts']})
  5. .then(function (category) {
  6. var posts = category.related('posts');
  7. res.json({error: false, data: posts.toJSON()});
  8. })
  9. .catch(function (err) {
  10. res.status(500).json({error: true, data: {message: err.message}});
  11. });
  12. });
  13. router.route('/posts/tag/:slug')
  14. .get(function (req, res) {
  15. Tag.forge({slug: req.params.slug})
  16. .fetch({withRelated: ['posts']})
  17. .then(function (tag) {
  18. var posts = tag.related('posts');
  19. res.json({error: false, data: posts.toJSON()});
  20. })
  21. .catch(function (err) {
  22. res.status(500).json({error: true, data: {message: err.message}});
  23. });
  24. });

The /posts/category/:id route will allow us to only fetch posts from a particular category while the /posts/tag/:slug route will only fetch posts from a particular tag.

That's it, our API is done! Let's wrap up by adding the routes to the main appliction and creating a server.

  1. app.use('/api', router);
  2. app.listen(3000, function() {
  3. console.log("✔ Express server listening on port %d in %s mode", 3000, app.get('env'));
  4. });

If you were following the article closely you would have noticed that I left out the PUT /posts/:id and DELETE /posts/:id routes - that's your homework, implement the 2 routes to complete your API.

Postman is a great Google Chrome extension for communicating with restful APIs - install it if you don't already have it and start testing your API.

You can download all the code used in this article and the previous one from Github: github.com/qawemlilo/node-mysql.

If you like my content, please consider buying me a coffee.

Buy Me A Coffee