Building the TopBlogger API with LoopBack

Last week I published an article outling how I rewrote an existing Express API using Loopback, resulting in 75% less code. I described how we initially utilized the topcoder community to design and build the API and the benefits of LoopBack versus simply using Express and Mongoose. In this post, I'll walk through the process of building the "TopBlogger" API with LoopBack and where it saved time and made code obselete.

I'll trying cover the application from the ground up, but you might want to peek at the previous post for the complete details. You can find all of the code referenced in this article at this repo.

Requirements

The TopBlogger application has two models: Blog and Comment. We'll be using the LoopBack provided User object for authentication and authorization but you can extend this if you'd like more functionality. It works just fine out of the box for what we need. The models have the following relations:

  • A blog belongs to a user
  • A blog can have many comments
  • A comment belongs to a user

The functional requirements for the blogging API are fairly straight forward. When blogging, everyone can typically view blogs and comments but any meaningful interaction requires authentication. Once logged in users can create new blogs, edit blogs that they authored, delete an unpublished blog that they authored, up or down-vote blogs not authored by themselves, comment on blogs and like or dislike comments not authored by themselves. No one is allowed to delete comments. That would just be crazy.

Given that people want to find and read blog entries, a fair amount of work is needed for discovery. We need endpoints for keyword search, newest blogs, trending blogs, most popular blogs and blogs by author and tags. All of this of course with pagination. We also want permalinks so that people can access a blog by the author username and slug (e.g., http://topblogger.com/jeffdonthemic/hello-world).

Scaffold the Application

The first thing we need to do is install the LoopBack CLI. Run npm install -g strongloop, then get up and grab a cup of coffee, watch some cat videos or take a run. The install takes a while.

Now we are ready to create a new application. Run slc loopback to start the generator and enter topblogger for the name of the application and directory. This will scaffold the API and run npm install. To smoke test just change to the new directory and run node . You should see the running application at http://localhost:3000!

Add MongoDB Database

LoopBack offers a number of data providers, in memory being the simpliest, but we're going to be using MongoDB. We can use the Data source generator to set this up for us.

npm install loopback-connector-mongodb --save
slc loopback:datasource

Enter topblogger as the name and choose the MongoDB connector. Next we'll need to add our connection parameters. Assuming you are running MongoDB locally, edit your server/datasources.json file so that it looks something like:

{
 "db":{
  "name":"db",
  "connector":"memory"
 },
 "topblogger":{
  "host":"localhost",
  "port":27017,
  "database":"topblogger",
  "username":"",
  "password":"",
  "name":"topblogger",
  "connector":"mongodb"
 }
}

Defining Models

Models are at the heart of LoopBack. When you connect a model to a persistent data source, LoopBack implements all of the CRUD operations needed to interact with the database and exposes the REST endpoints automatically. No need to write handlers for each endpoint! We can then add application logic to models, boot scripts or middleware to round out our functionality!

Use the LoopBack model generator to build the Blog and Comment models below.

slc loopback:model
? Enter the model name: Blog
? Select the data-source to attach Blog to: topblogger (mongodb)
? Select model's base class: PersistedModel
? Expose Blog via the REST API? Yes
? Custom plural form (used to build REST URL): blogs

Follow the prompts to create the properties below. Create the Comment model the same way along with its properties. It's not rocket surgery.

Blog

  • title (string)
  • content (string)
  • tags (array of strings)
  • slug (string)
  • numberOfUpVotes (number)
  • numberOfDownVotes (number)
  • numberOfViews (number)
  • upvotes (array of strings)
  • downvotes (array of strings)
  • isPublished (boolean)
  • createdDate (date)
  • lastUpdatedDate (date)

Comment

  • content (string)
  • numOfLikes (number)
  • numOfDislikes (number)
  • likes (array of strings)
  • dislikes (array of strings)
  • createdDate (date)
  • lastUpdatedDate (date)

Create the Relations

One of the great things about Rails is the functionality to easily implement and use relations between models. LoopBack offers similar functionality. The framework offers BelongsTo, HasMany, HasManyThrough, HasAndBelongsToMany, Polymorphic and Embedded relations that allow you to connect, query, expose endpoints and perform all sorts of nifty functional with models.

Since users write blogs, create a BelongsTo relation to User to define the "author" of each Blog.

[topblogger]$ slc loopback:relation
? Select the model to create the relationship from: Blog
? Relation type: belongs to
? Choose a model to create a relationship with: User
? Enter the property name for the relation: author
? Optionally enter a custom foreign key:

Readers love commenting on blogs so we need to allow a blog to have many comments. Create a HasMany relation so that we can associate an array of Comments to a Blog.

[topblogger]$ slc loopback:relation
? Select the model to create the relationship from: Blog
? Relation type: has many
? Choose a model to create a relationship with: Comment
? Enter the property name for the relation: comments
? Optionally enter a custom foreign key:
? Require a through model? No

And lastly we'll need to create the same type of author relation for Comments that we did for the Blog above. Every comment must be authored by a user.

[topblogger]$ slc loopback:relation
? Select the model to create the relationship from: Comment
? Relation type: belongs to
? Choose a model to create a relationship with: User
? Enter the property name for the relation: author
? Optionally enter a custom foreign key:

Implementing Access Control

One of the most powerful features of LoopBack is access control. LoopBack applications access data through models, so controlling access to data means putting restrictions on models. LoopBack access controls are determined by access control lists or ACLs which specify who or what can read or write data and execute methods. Our application only consists of the two following types of users:

Unauthenticated

Unauthenticated users can only view blogs and comments.

Authenticated

LoopBack provides all sorts of user related functionality for us like signup, login, logout, forgot password and much more. We'll gladly use this! Once logged in, users have the same functionality as unauthenticated user plus the ability to:

  • Create a new blog
  • Edit blogs where they are the author
  • Publish a blog where they are the author
  • Upvote a blog where they are not the author
  • Downvote a blog where they are not the author
  • Create a comment for a blog
  • Edit a comment where they are the author
  • Like a comment where they are not the author
  • Unlike a comment where they are not the author

We start by denying access to all endpoints to everyone for Blogs and Comments. Then we'll go through and authorize select endpoints where appropriate.

[topblogger]$ slc loopback:acl
? Select the model to apply the ACL entry to: Blog
? Select the ACL scope: All methods and properties
? Select the access type: All (match all types)
? Select the role: All users
? Select the permission to apply: Explicitly deny access

[topblogger]$ slc loopback:acl
? Select the model to apply the ACL entry to: Comment
? Select the ACL scope: All methods and properties
? Select the access type: All (match all types)
? Select the role: All users
? Select the permission to apply: Explicitly deny access

Allow everyone read access to Blogs and Comments.

? Select the model to apply the ACL entry to: Blog
? Select the ACL scope: All methods and properties
? Select the access type: Read
? Select the role: All users
? Select the permission to apply: Explicitly grant access

? Select the model to apply the ACL entry to: Comment
? Select the ACL scope: All methods and properties
? Select the access type: Read
? Select the role: All users
? Select the permission to apply: Explicitly grant access

Only authenticated users can create new Blog records.

? Select the model to apply the ACL entry to: Blog
? Select the ACL scope: All methods and properties
? Select the access type: Write
? Select the role: Any authenticated user
? Select the permission to apply: Explicitly grant access

Make sure that only a Blog's author can edit and publish it. The publish method is a custom remote method that we'll add to the Blog model later.

? Select the model to apply the ACL entry to: Blog
? Select the ACL scope: All methods and properties
? Select the access type: Write
? Select the role: The user owning the object
? Select the permission to apply: Explicitly grant access

? Select the model to apply the ACL entry to: Blog
? Select the ACL scope: A single method
? Enter the method name: publish
? Select the role: The user owning the object
? Select the permission to apply: Explicitly grant access

We'll define upvote and downvote remote methods later but we'll add the security now by first blocking all access to the endpoints and then letting any authenticated user have access to them. We also have a requirement that users cannot upvote/downvote their own Blog but we'll handle that outside of the ACL in code.

? Select the model to apply the ACL entry to: Blog
? Select the ACL scope: A single method
? Enter the method name: upvote
? Select the role: All users
? Select the permission to apply: Explicitly deny access

? Select the model to apply the ACL entry to: Blog
? Select the ACL scope: A single method
? Enter the method name: upvote
? Select the role: Any authenticated user
? Select the permission to apply: Explicitly grant access

? Select the model to apply the ACL entry to: Blog
? Select the ACL scope: A single method
? Enter the method name: downvote
? Select the role: All users
? Select the permission to apply: Explicitly deny access

? Select the model to apply the ACL entry to: Blog
? Select the ACL scope: A single method
? Enter the method name: downvote
? Select the role: Any authenticated user
? Select the permission to apply: Explicitly grant access

And finally, we'll specify that only authenticated users can create and edit Comments.

? Select the model to apply the ACL entry to: Comment
? Select the ACL scope: All methods and properties
? Select the access type: Write
? Select the role: Any authenticated user
? Select the permission to apply: Explicitly grant access

Now that were done, take a peek at Blog and Comment JSON files in common/models to see how LoopBack generated the ACL section.

Adding Custom Remote Methods

LoopBack exposes models endpoints automatically but at some point you'll want provide additional functionalty besides just CRUDing records. LoopBack makes this extremely simple with remote methods. For the Blog model we need to expose /blogs/:id/publish, /blogs/:id/upvote and /blogs/:id/downvote endpoints. I'll just touch on the important pieces so be sure to check out the /common/models/blog.js file for all of the code.

We start off the remote method by register a PUT method called publish that accepts the blog's ID in the query string and returns the updated blog object. This code also sets up the method in the Swagger Explorer for easy testing. The Blog.publish function handles the actual request for the endpoint. It simply finds the blog record in MongoDB, sets a few properties and commits it. The security for the endpoint was setup earlier in the ACL.

// Register a 'publish' remote method: /blogs/:id/publish
Blog.remoteMethod(
 'publish',
 {
  http: {path: '/:id/publish', verb: 'put'},
  accepts: {arg: 'id', type: 'string', required: true, http: { source: 'path' }},
  returns: {root: true, type: 'object'},
  description: 'Marks a blog as published.'
 }
);

// the actual function called by the route to do the work
Blog.publish = function(id, cb) {
 Blog.findById(id, function(err, record){
  record.updateAttributes({isPublished: true, publishedDate: new Date()}, function(err, instance) {
 if (err) cb(err);
 if (!err) cb(null, instance);
  })
 })
};

The upvote remote method is similar but has a little more substance to it. This time we register a POST method that, again, accepts the blog's ID in the query string and returns a the updated blog object. However, we added a remote hook that runs before the remote method is called. If the caller is either the author of the blog or has already upvoted the blog, then it returns a 403 error. Else it executes the remote method to increment the number of upvotes and adds the caller to the array of users that upvoted the blog.

Blog.remoteMethod(
 'upvote',
 {
  http: {path: '/:id/upvote', verb: 'post'},
  accepts: {arg: 'id', type: 'string', required: true, http: { source: 'path' }},
  returns: {root: true, type: 'object'},
  description: 'Marks a blog as upvoted.'
 }
);

// Remote hook called before running function
Blog.beforeRemote('upvote', function(ctx, user, next) {
 Blog.findById(ctx.req.params.id, function(err, record){
  // do not let the user upvote their own record
  if (record.authorId === ctx.req.accessToken.userId) {
 var err = new Error("User cannot upvote their own blog post.");
 err.status = 403;
 next(err);
  // do no let the user upvote a comment more than once
  } else if (record.upvotes.indexOf(ctx.req.accessToken.userId) != -1) {
 var err = new Error("User has already upvoted the blog.");
 err.status = 403;
 next(err);
  } else {
 next();
  }
 })
});

// the actual function called by the route to do the work
Blog.upvote = function(id, cb) {
 // get the current context
 var ctx = loopback.getCurrentContext();
 Blog.findById(id, function(err, record){
  // get the calling user who 'upvoted' it from the context
  record.upvotes.push(ctx.active.accessToken.userId);
  record.updateAttributes({numOfUpVotes: record.upvotes.length, upvotes: record.upvotes}, function(err, instance) {
 if (err) cb(err);
 if (!err) cb(null, instance);
  })
 })
};

Removing Functionality

With LoopBack its super simply to hide methods and endpoints. Since we don't want anyone to be able to delete comments, we simply add the following to the Comment model:

Comment.disableRemoteMethod('deleteById', true);

Adding Custom Express Routes

Our last requirement is enable permalinks so that people can access a blog by the author's username and slug (e.g., http://topblogger.com/jeffdonthemic/hello-world) instead of by Blog ID. However, since the endpoint is not directly tied to a model we'll need to use a boot script to add our new custom route.

The /server/boot/routes.js file holds all of the custom routes (all one of them) and is loaded when the applications starts. Looking very similar to an Express route (because it is), this GET method uses the user and slug values from the query string, finds the record and its associated comments in MongoDB and returns it the same way it would if fetched by Blog ID.

 app.get('/:user/:slug', function(req, res) {
  Blog.findOne({ where: {authorId: req.params.user, slug:req.params.slug}, include: 'comments'}, function(err, record){
 if (err) res.send(err);
 if (!err && record) {
  res.send(record);
 } else {
  res.send(404);
 }
  });
 });

Code Comparison

So just exactly where did LoopBack save us time? Well, for instance, the LoopBack blog model code looks much more succinct and pleasing to the eye than the original blog model code. The original code handles CRUD functional and is explicitly checking for access and returning status codes (400, 401, 403, 404, etc.) that LoopBack handles. My custom code decreased from 1,272 lines to 314 lines! Wow!

Testing was much simplier with LoopBack. Our mocha tests went from 1,599 lines to 380 lines. This substantial reduction in test code will make life much easier in the future for enhancements and debugging! Based upon my requirements, I simply set up my tests for both unauthenticated and authenticated users.

Conclusion

LoopBack is great for building APIs and it's a huge productivity gain. However, coupled with the other services StrongLoop offers, it's an impressive suite of tools with which to build applications.