JeanCarl's Adventures

Remind Me API with Apiary

July 28, 2015 |

Remind Me was my first project in my 15 projects in 30 days challenge. It was a web app that allowed you to enter a phone number, message, and choose a date and time for the message to be texted.

I took a second look at this project for the DevPost First API challenge sponsored by Apiary. Apiary is a platform that helps you create an API and maintain up-to-date documentation.

If you’ve ever used an API before, you may have come across documentation that lagged behind the actual implementation. Good API documentation also provides examples of request and response bodies and how you can interact with the API. Apiary helps to solve these problems when creating an API.

Remind Me was a great example of a project that could use an API. Here’s the process I took to make Remind Me accessible as both a web app, and also as an API.

Setup

I modified the original project to clean up some things and make API endpoints more intuitive. There are three files for this project. app.js is the Node.js backend that handles the web interface and also the API endpoints. The AngularJS app, index.html and remindme.js provide the web app interface.

// Filename: app.js

// Twilio Account SID
var TWILIO_ACCOUNT_SID = '';

// Twilio Auth Token
var TWILIO_AUTH_TOKEN = '';

// Twilio phone number to send SMS from.
var TWILIO_NUMBER = '';

// Set to how frequently the queue should be checked.
var frequencyMilliseconds = 5000;

// Mongo DB server address
var mongooseServerAddress = 'mongodb://127.0.0.1:27017/test';

// Port
var PORT = 8080;

/*********** End Configuration ***********/

var express = require('express');
var app = express();
var bodyParser = require('body-parser');
var url = require('url');
var mongoose = require('mongoose');

app.use(bodyParser.json());
app.listen(PORT);

mongoose.connect(mongooseServerAddress);

var Reminder = mongoose.model('Reminder', {
  message: String,
  sendon: Number,
  to: String
});

var client = require('twilio')(TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN);

// Create a new reminder.
app.post('/api/reminders', function(req, res) {
  var time = new Date(req.body.sendon);

  Reminder.create({
    message: req.body.message,
    sendon: time.getTime(),
    to: req.body.to
  }, function(err, reminder) {
    if(err) {
      res.send({'error': 'Could not create reminder.'});
      return;
    }

    res.send({
      'message': reminder.message, 
      'sendon': reminder.sendon, 
      'to':reminder.to, 
      'id': reminder._id
    });
  });
});

// Get a reminder.
app.get('/api/reminders/:id', function(req, res) {
  Reminder.findOne({_id: req.params.id}, function(err, reminder) {
    if(err || !reminder) {
      res.statusCode = 404;
      res.send({'error': 'Reminder not found'});
    } else {
      res.send({
        'message': reminder.message, 
        'sendon': reminder.sendon, 
        'to':reminder.to, 
        'id': reminder._id
      });
    }
  });
});

// Cancel a reminder.
app.post('/api/reminders/:id/remove', function(req, res) {
  // To make the default test succeed, this returns a success response and doesn't remove a reminder. 
  if(req.params.id == '55b6858b50f3e68f4d48dd41') {
    res.send({'status': 'success'});
    return;
  }

  Reminder.findOne({_id: req.params.id}, function(err, reminder) {
    if(err || !reminder) {
      res.send({'error': 'Reminder not found'});
      return;
    }
  
    Reminder.remove({_id: req.params.id}, function(err) {
      if(err)
        res.send({'error': 'Reminder not found'});

      res.send({'status': 'success'});
    });
  });
});

// Get list of reminders for phone number.
app.get('/api/phone/:number/reminders', function(req, res) {
  Reminder.find({to: req.params.number}, function(err, reminders) {
    if(err) {
      res.send({'reminders':[]});
      return;
    }

    var result = [];

    for(var i in reminders) {
      result.push({
        'sendon': reminders[i].sendon, 
        'message': reminders[i].message,
        'id': reminders[i]._id
      });
    }

    res.send({'reminders': result});
  });
});

setInterval(function() {
  var timeNow = new Date();
  console.log(timeNow.getTime());

  // Find any reminders that have already passed, process them, and remove them from the queue.
  Reminder.find({'sendon': {$lt: timeNow.getTime()}}, function(err, reminders) {
    if(err)  {
      console.log(err);
      return;
    }

    if(reminders.length == 0) {
      console.log('no messages to be sent');
      return;
    }

    reminders.forEach(function(message) {
      client.messages.create({
          body: message.message,
          to: '+1'+message.to,
          from: '+1'+TWILIO_NUMBER
      }, function(err, sms) {
        if(err) {
          console.log(err);
          return;
        }

        console.log('sending '+message.message+' to '+message.to+' ('+sms.sid+')');
      });
      
      Reminder.remove({_id: message._id}, function(err) {
        console.log(err)
      });
    });
  });
}, frequencyMilliseconds);

app.use(express.static(__dirname + '/public'));

console.log('App listening on port '+PORT);
// Filename: public/remindme.js

angular.module('ReminderApp', [])
.controller('ReminderListCtrl', ['$scope', '$http', '$filter', function($scope, $http, $filter) {
  $scope.reminderDate = new Date();
  $scope.reminderTime = new Date();
  $scope.reminderTime.setMilliseconds(0);
  $scope.reminderTime.setSeconds(0);
  $scope.syncing = false;

  $scope.reminders = [];

  // Get reminders from the server.
  $scope.fetchList = function() {
    if($scope.phonenumber.length == 0) 
      return;

    $scope.syncing = true;
    
    $http.get('/api/phone/'+$scope.phonenumber+'/reminders').success(function(response) {
      $scope.reminders = response.reminders;
      $scope.syncing = false;
    });
  };

  $scope.addReminder = function() {
    var reminderDateTime = new Date($filter('date')($scope.reminderDate, 'yyyy-MM-dd')+" "+$filter('date')($scope.reminderTime, 'HH:mm'));

    var data = {
      message: $scope.reminderText, 
      sendon: reminderDateTime.getTime(), 
      to: $scope.phonenumber
    };

    $http.post('/api/reminders', data).success(function(response) {
      $scope.reminders.push(response);
      $scope.reminderText = '';
    });
  };

  $scope.removeReminder = function(reminder) {
    var oldReminders = $scope.reminders;

    $scope.reminders = [];
    angular.forEach(oldReminders, function(r) {
      if(reminder.id != r.id) {
        $scope.reminders.push(r);
      }
    });

    $http.post('/api/reminders/'+reminder.id+'/remove');
  };
}]);
<!-- Filename: public/index.html -->

<!DOCTYPE html> 
<html ng-app="ReminderApp">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1"> 
    <title>Remind Me!</title> 
    <link rel="stylesheet" href="http://code.jquery.com/mobile/1.1.1/jquery.mobile-1.1.1.min.css" />
    <script src="http://code.jquery.com/jquery-1.7.1.min.js"></script>
    <script src="http://code.jquery.com/mobile/1.1.1/jquery.mobile-1.1.1.min.js"></script>
    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.16/angular.min.js"></script>
    <script src="remindme.js"></script>
  </head> 

  <body> 

    <div data-role="page">

      <div data-role="header" data-theme="b">
        ## Remind Me!
      </div><!-- /header -->

      <div data-role="content"> 
        <div ng-controller="ReminderListCtrl">
          <h3>My Phone #</h3>
          <input type="text" ng-model="phonenumber" ng-blur="fetchList()" />
          
          <h3>My Reminders <div ng-show="syncing">Syncing...</div></h3>
          <ul data-role="listview" data-inset="true" data-split-icon="delete" data-split-theme="d">
            <li ng-repeat="reminder in reminders | orderBy:'time'">
              <a href="#" ><h2>{{reminder.message}}</h2>
              <p>{{reminder.sendon | orderBy: 'reminder.sendon' | date:"MM/dd hh:mm a"}}</p>
              <a href="#" ng-click="removeReminder(reminder)" data-rel="popup" data-position-to="window" data-transition="pop">Remove Reminder</a>
            </a>
            </li>
          </ul>
          <div ng-hide="reminders.length > 0">
            You have no reminders!
          </div>
          
          <form ng-show="phonenumber.length == 10">
            <h3>New Reminder</h3>
            <input type="text" ng-model="reminderText"  size="30"
                   placeholder="remind me about..."> 
            <div class="ui-grid-a">
              <div class="ui-block-a">
                on<br />
                <input type="date" ng-model="reminderDate" />
              </div>
              <div class="ui-block-b">
                at<br />
                <input type="time" ng-model="reminderTime"/>
              </div>
            </div>
            <input class="btn-primary" type="button" ng-click="addReminder()" value="Add">
          </form>
        </div>
      </div><!-- /content -->
      
      <div data-role="footer" data-theme="b">
        <h4>Copyright (c) JeanCarl Bisson</h4>
      </div><!-- /footer -->
      
    </div><!-- /page -->
  </body>
</html>
// Filename: package.json
{
  "name": "remind-me-api",
  "description": "Remind Me API for Node.js",
  "version": "0.0.1",
  "private": true,
  "dependencies": {
    "body-parser": "",
    "express": "*",
    "url": "*",
    "mongoose": "*",
    "twilio": "*"
  }
}

Install the Node.js dependencies:

npm install

And run the Node.js app:

nodejs app.js

Apiary

Apiary can be used in several different ways. You can design your API from the ground up with mock data from the mock server, or you can add existing API endpoints into the documentation.

On the left column is an editor to build the Blueprint. On the right column is the rendered documentation.

Photo

Since I already had endpoints to add, remove, and list reminders, I renamed them and documented them in the API Blueprint. While constructing the blueprint, I realized that the list of reminders would be better under a phone endpoint instead of under a reminder endpoint.

/phone/*****/reminders

This would allow future expansion for a phone to have other objects along with the reminders. I took the traditional approach of using the GET HTTP method to retrieve a reminder, and POST HTTP method to create a new reminder. On the right column, you can choose an API endpoint and execute it.

Photo

Going back to the web app, the reminder has been added.

Photo

I chose to add a remove endpoint for the reminder entity instead of using the DELETE HTTP method for simplicity. Again, I ran a test to the API to make sure the API was working as expected.

Photo

Using mock data helped to visualize exactly what data should be returned and if a name should be changed. Then changing the endpoint to my production API ensured everything was consistent.

Here’s the API Blueprint (change the 0.0.0.0:8080 host value to point to where you host Remind Me):

FORMAT: 1A
HOST: http://0.0.0.0:8080/

# Remind Me

Remind Me is an API to set up reminders.

## Group Phone

### Reminders [GET /api/phone/{phone_number}/reminders]

Returns queued reminders for specified phone number

+ Parameters
    + phone_number (required, string, `0000000000`) phone number to find reminders for

+ Response 200 (application/json; charset=utf-8)

        {"reminders":[{"id":"55b6858b50f3e68f4d48dd41","message":"testing","sendon":"2018-07-31 06:38:48"}]}

## Group Reminders

Reminders represent messages that are sent to a phone number at a specified time.

A Reminder consists of the following:
- message (string) the message to SMS
- to (string) the phone number to send the SMS to
- sendon (string) the date and time when the SMS should be sent

### Get reminder details [GET /api/reminders/{reminder_id}]

Returns the details of the reminder, including the message to be sent, where the SMS will be sent to, and when the SMS will be sent.

+ Parameters
    + reminder_id (required, string, `55b6858b50f3e68f4d48dd41`) ID of the reminder

+ Response 200 (application/json; charset=utf-8)

        {"id":"55b6858b50f3e68f4d48dd41","message":"testing","to":"0000000000"}


### Create a reminder [POST /api/reminders]

Create a reminder that will send a message to the specified phone number on the specified date.

+ Request (application/json)

    + Body
    
            {"message":"testing","to":"0000000000","sendon":"2018-07-27 13:00:00"}
        
+ Response 200 (application/json; charset=utf-8)

        {"id":"55b6858b50f3e68f4d48dd41","message":"testing","to":"0000000000"}

### Cancel a reminder [POST /api/reminders/{reminder_id}/remove]

Removes the reminder from the queue so that it is not sent.

+ Parameters
    + reminder_id (required, string, `55b6858b50f3e68f4d48dd41`) ID of the reminder

+ Request (application/json)

+ Response 200 (application/json; charset=utf-8)

        {"status":"success"}

Lastly, you can run tests against the production API to make sure it matches with the documentation. Some of the tests failed at first and made it easy to fix the inconsistencies before they were live and caused issues that might have been harder to track down.

Install Dredd, an HTTP API Testing Framework:

npm install -g dredd

And then run the test:

dredd apiary.apib

Photo

Using the diff tool after the tests run, you can see the differences and take appropriate action.

Photo

Source code

You can find the repo on Github.

Post Mortem

Apiary was a little confusing to use at first. The API Blueprint syntax takes a little time to get used to. But once you understand it, the process is pretty smooth.

I really liked the mock data where Apiary will mock up all the sample responses at an accessible URL. Theoretically, you could then access this URL and play around with the experience of being a client user.

Lastly, testing using the dredd command line tool was great. After making a small change, I would run the tests again to see if everything functions still. It adds confidence to the system.