Employee Directory with Node.js and Express

Download the Code

Learn how to implement an employee directory that you can query using SMS. Request information from anyone at your company just by sending a text message to a Twilio Number

Here is how it works at a high level:

  • The user sends a SMS with an Employee's name to the Twilio number.
  • The user receives information for the requested Employee.

Create an Employee Model

The first thing we need is a collection of employees. We will be using Mongoose for this.

Our employee entity has a few fields for contact information, including their name, phone number, and a public URL containing an image of them.

Loading Code Samples...
Language
'use strict';

var mongoose = require('mongoose');

var Employee = new mongoose.Schema({
  fullName: String,
  email: String,
  phoneNumber: String,
  imageUrl: String
});

module.exports = mongoose.model('employee', Employee);
models/employee.js
Employee Model

models/employee.js

Now that we have a model that represents an employee, let's see how to search for employees by name.

Search for an Employee by Name

The employee-finder module allows us to search the database for employees either by name or by their unique database identifier. When searching by name, we'll return a list of employees whose name might match a search query, or just one if we find an exact match. If we know the ID of the employee we're looking for, we can return it right away.

Loading Code Samples...
Language
'use strict';

var Employee = require('../models/employee');

var findByName = function(name, callback) {
  Employee.find({
    "fullName": {
      "$regex": name, "$options": "i"
    }
  }, callback).sort("fullName");
};

var findById = function(id, callback) {
  Employee.findOne({
    "_id": id
  }, callback);
};

module.exports.findByName = findByName;

module.exports.findById = findById;
lib/employee-finder.js
Employee Search Service

lib/employee-finder.js

Now, let's use this search functionality when responding to an SMS from a user.

Receive an Incoming SMS

When your number receives an SMS message, Twilio will send an HTTP POST request to our application. This will be handled by the /directory/search/ route.

We check for the cookie and numeric input (line 15/more on that later) or perform a query for the desired employee. The results are packaged up as a TwiML response through the twiml-generator module and sent back to Twilio and, in turn, the original sender of the SMS.

Loading Code Samples...
Language
'use strict';

var express = require('express')
  , router = express.Router()
  , twilio = require('twilio')
  , employeeFinder = require('../lib/employee-finder')
  , _ =  require('underscore')
  , twimlGenerator = require('../lib/twiml-generator');

// POST /directory/search/
router.post('/search/', function(req, res, next) {
  var body = req.body.Body;
  res.type('text/xml');

  if (req.cookies.cachedEmployees !== undefined && !isNaN(body)) {
    var cachedEmployees = req.cookies.cachedEmployees;
    var employeeId = cachedEmployees[body];
    if (employeeId === undefined) {
      res.send(twimlGenerator.notFound().toString());
    } else {
      employeeFinder.findById(employeeId, function(err, employee) {
        res.clearCookie('cachedEmployees');
        res.send(twimlGenerator.singleEmployee(employee).toString());
      });
    }
  } else {
    employeeFinder.findByName(body, function(err, employees) {
      if (employees.length === 0) {
        res.send(twimlGenerator.notFound().toString());
      } else if (employees.length === 1) {
        res.send(twimlGenerator.singleEmployee(employees[0]).toString());
      } else {
        var options = _.map(employees, function(it, index) {
          return { option: index + 1, fullName: it.fullName, id: it.id };
        });
        var cachedEmployees = _.object(_.map(options, function(it) { return [it.option, it.id]; }));
        res.cookie('cachedEmployees', cachedEmployees, { maxAge: 1000 * 60 * 60 });

        res.send(twimlGenerator.multipleEmployees(options).toString());
      }
    });
  }
});

module.exports = router;
routes/directory.js
Employee Directory Route

routes/directory.js

Now that we have our search route, let's see how we can request a specific employee by name.

Respond with a Single Match for an Employee Name

Let's say it finds a single employee matching the text message. In this case, we simply write out a response that contains the employee's contact information, including a photo, making our response a MMS message.

A single matching employee isn't the only scenario, however.

Loading Code Samples...
Language
'use strict';

var MessagingResponse = require('twilio').twiml.MessagingResponse,
  _ = require('underscore');

var notFound = function() {
  var resp = new MessagingResponse();
  resp.message('We did not find the employee you\'re looking for');
  return resp;
};

var singleEmployee = function(employee) {
  var resp = new MessagingResponse();
  var message = resp.message();
  message.body(`${employee.fullName}\n${employee.phoneNumber}\n${employee.email}`);
  message.media(employee.imageUrl);
  return resp;
};

var multipleEmployees = function(employees) {
  var resp = new MessagingResponse();
  var optionsMessage = _.reduce(employees, function(memo, it) {
    return memo += `\n${it.option} for ${it.fullName}`;
  }, '');

  resp.message(`We found multiple people, reply with:${optionsMessage}\nOr start over`);
  return resp;
};

module.exports.notFound = notFound;

module.exports.singleEmployee = singleEmployee;

module.exports.multipleEmployees = multipleEmployees;
TwiML helper methods for creating messages.
Get Twilio Response For Search Result

TwiML helper methods for creating messages.

As you can see, The Twilio Node.js Helper Library simplifies the way you can generate and send SMS messages. Together with a simple database query, the application is able to return valuable information over SMS. Let's see how we handle multiple or no results next.

Handle Multiple or No Results

If we don't find any employees, we can simply return a "Not found" message.

What about multiple matches? For this case, we want to return a list of the matching employees' names along with an incrementing number the end user can use to make their selection. For example, if someone searched for "Man" they might get something like:

We found: 1-Spider-Man, 2-Iron Man
 - Reply with # of desired person
Loading Code Samples...
Language
'use strict';

var MessagingResponse = require('twilio').twiml.MessagingResponse,
  _ = require('underscore');

var notFound = function() {
  var resp = new MessagingResponse();
  resp.message('We did not find the employee you\'re looking for');
  return resp;
};

var singleEmployee = function(employee) {
  var resp = new MessagingResponse();
  var message = resp.message();
  message.body(`${employee.fullName}\n${employee.phoneNumber}\n${employee.email}`);
  message.media(employee.imageUrl);
  return resp;
};

var multipleEmployees = function(employees) {
  var resp = new MessagingResponse();
  var optionsMessage = _.reduce(employees, function(memo, it) {
    return memo += `\n${it.option} for ${it.fullName}`;
  }, '');

  resp.message(`We found multiple people, reply with:${optionsMessage}\nOr start over`);
  return resp;
};

module.exports.notFound = notFound;

module.exports.singleEmployee = singleEmployee;

module.exports.multipleEmployees = multipleEmployees;
TwiML helper methods for creating messages.
Get TwiML for Multiple Results

TwiML helper methods for creating messages.

Let's see how these options are stored next.

Cache the List of Possible Matches

For the message text returned to the user, we build a numbered menu of possible matches.

Our app needs to remember — between SMS messages from the user — the mapping of the 1, 2, 3 selection numbers to the actual unique ID's of employees. You will notice we are placing them in a cookie, which Twilio will send back with every HTTP request to our application.

Loading Code Samples...
Language
'use strict';

var express = require('express')
  , router = express.Router()
  , twilio = require('twilio')
  , employeeFinder = require('../lib/employee-finder')
  , _ =  require('underscore')
  , twimlGenerator = require('../lib/twiml-generator');

// POST /directory/search/
router.post('/search/', function(req, res, next) {
  var body = req.body.Body;
  res.type('text/xml');

  if (req.cookies.cachedEmployees !== undefined && !isNaN(body)) {
    var cachedEmployees = req.cookies.cachedEmployees;
    var employeeId = cachedEmployees[body];
    if (employeeId === undefined) {
      res.send(twimlGenerator.notFound().toString());
    } else {
      employeeFinder.findById(employeeId, function(err, employee) {
        res.clearCookie('cachedEmployees');
        res.send(twimlGenerator.singleEmployee(employee).toString());
      });
    }
  } else {
    employeeFinder.findByName(body, function(err, employees) {
      if (employees.length === 0) {
        res.send(twimlGenerator.notFound().toString());
      } else if (employees.length === 1) {
        res.send(twimlGenerator.singleEmployee(employees[0]).toString());
      } else {
        var options = _.map(employees, function(it, index) {
          return { option: index + 1, fullName: it.fullName, id: it.id };
        });
        var cachedEmployees = _.object(_.map(options, function(it) { return [it.option, it.id]; }));
        res.cookie('cachedEmployees', cachedEmployees, { maxAge: 1000 * 60 * 60 });

        res.send(twimlGenerator.multipleEmployees(options).toString());
      }
    });
  }
});

module.exports = router;
routes/directory.js
Get TwiML Response for Employee List

routes/directory.js

When the user that queried the employee directory receives the message with a list of employees, they will text back a number that corresponds to the result on the list that they are interested in querying further. Twilio will send a request to the webhook which handles incoming SMS messages. At this point, our app will try to parse the user's message to determine what to do next.

Return Employee's Contact Information by Number Choice

When we receive an SMS message, we check whether:

  • The body of the text is, in fact, a number.
  • A cookie exists with the mapping of numbers to id's.

If any of those checks fail, then we'll simply proceed with our typical name lookup.

Otherwise, it will check if the chosen option exists in the cookie. If it does, we return the single employee that matches their selection.

Loading Code Samples...
Language
'use strict';

var express = require('express')
  , router = express.Router()
  , twilio = require('twilio')
  , employeeFinder = require('../lib/employee-finder')
  , _ =  require('underscore')
  , twimlGenerator = require('../lib/twiml-generator');

// POST /directory/search/
router.post('/search/', function(req, res, next) {
  var body = req.body.Body;
  res.type('text/xml');

  if (req.cookies.cachedEmployees !== undefined && !isNaN(body)) {
    var cachedEmployees = req.cookies.cachedEmployees;
    var employeeId = cachedEmployees[body];
    if (employeeId === undefined) {
      res.send(twimlGenerator.notFound().toString());
    } else {
      employeeFinder.findById(employeeId, function(err, employee) {
        res.clearCookie('cachedEmployees');
        res.send(twimlGenerator.singleEmployee(employee).toString());
      });
    }
  } else {
    employeeFinder.findByName(body, function(err, employees) {
      if (employees.length === 0) {
        res.send(twimlGenerator.notFound().toString());
      } else if (employees.length === 1) {
        res.send(twimlGenerator.singleEmployee(employees[0]).toString());
      } else {
        var options = _.map(employees, function(it, index) {
          return { option: index + 1, fullName: it.fullName, id: it.id };
        });
        var cachedEmployees = _.object(_.map(options, function(it) { return [it.option, it.id]; }));
        res.cookie('cachedEmployees', cachedEmployees, { maxAge: 1000 * 60 * 60 });

        res.send(twimlGenerator.multipleEmployees(options).toString());
      }
    });
  }
});

module.exports = router;
routes/directory.js
Cached Employee Lookup Logic

routes/directory.js

Only thing left to do is celebrate.

winning

We have just implemented employee directory using Express. Now you can get your employee's information by texting a Twilio number.

Where to Next?

If you're a Node.js developer working with Twilio, you might also enjoy these tutorials:

Browser-Calls

Learn how to use Twilio Client to make browser-to-phone and browser-to-browser calls with ease.

ETA-Notifications

Learn how to implement ETA Notifications using Express and Twilio.

Did this help?

Thanks for checking out this tutorial! If you have any feedback to share with us, we'd love to hear it. Tweet @twilio to let us know what you think!

Jose Oliveros
David Prothero
Agustin Camino

Need some help?

We all do sometimes; code is hard. Get help now from our support team, or lean on the wisdom of the crowd browsing the Twilio tag on Stack Overflow.

1 / 1
Loading Code Samples...
'use strict';

var mongoose = require('mongoose');

var Employee = new mongoose.Schema({
  fullName: String,
  email: String,
  phoneNumber: String,
  imageUrl: String
});

module.exports = mongoose.model('employee', Employee);
'use strict';

var Employee = require('../models/employee');

var findByName = function(name, callback) {
  Employee.find({
    "fullName": {
      "$regex": name, "$options": "i"
    }
  }, callback).sort("fullName");
};

var findById = function(id, callback) {
  Employee.findOne({
    "_id": id
  }, callback);
};

module.exports.findByName = findByName;

module.exports.findById = findById;
'use strict';

var express = require('express')
  , router = express.Router()
  , twilio = require('twilio')
  , employeeFinder = require('../lib/employee-finder')
  , _ =  require('underscore')
  , twimlGenerator = require('../lib/twiml-generator');

// POST /directory/search/
router.post('/search/', function(req, res, next) {
  var body = req.body.Body;
  res.type('text/xml');

  if (req.cookies.cachedEmployees !== undefined && !isNaN(body)) {
    var cachedEmployees = req.cookies.cachedEmployees;
    var employeeId = cachedEmployees[body];
    if (employeeId === undefined) {
      res.send(twimlGenerator.notFound().toString());
    } else {
      employeeFinder.findById(employeeId, function(err, employee) {
        res.clearCookie('cachedEmployees');
        res.send(twimlGenerator.singleEmployee(employee).toString());
      });
    }
  } else {
    employeeFinder.findByName(body, function(err, employees) {
      if (employees.length === 0) {
        res.send(twimlGenerator.notFound().toString());
      } else if (employees.length === 1) {
        res.send(twimlGenerator.singleEmployee(employees[0]).toString());
      } else {
        var options = _.map(employees, function(it, index) {
          return { option: index + 1, fullName: it.fullName, id: it.id };
        });
        var cachedEmployees = _.object(_.map(options, function(it) { return [it.option, it.id]; }));
        res.cookie('cachedEmployees', cachedEmployees, { maxAge: 1000 * 60 * 60 });

        res.send(twimlGenerator.multipleEmployees(options).toString());
      }
    });
  }
});

module.exports = router;
'use strict';

var MessagingResponse = require('twilio').twiml.MessagingResponse,
  _ = require('underscore');

var notFound = function() {
  var resp = new MessagingResponse();
  resp.message('We did not find the employee you\'re looking for');
  return resp;
};

var singleEmployee = function(employee) {
  var resp = new MessagingResponse();
  var message = resp.message();
  message.body(`${employee.fullName}\n${employee.phoneNumber}\n${employee.email}`);
  message.media(employee.imageUrl);
  return resp;
};

var multipleEmployees = function(employees) {
  var resp = new MessagingResponse();
  var optionsMessage = _.reduce(employees, function(memo, it) {
    return memo += `\n${it.option} for ${it.fullName}`;
  }, '');

  resp.message(`We found multiple people, reply with:${optionsMessage}\nOr start over`);
  return resp;
};

module.exports.notFound = notFound;

module.exports.singleEmployee = singleEmployee;

module.exports.multipleEmployees = multipleEmployees;
'use strict';

var MessagingResponse = require('twilio').twiml.MessagingResponse,
  _ = require('underscore');

var notFound = function() {
  var resp = new MessagingResponse();
  resp.message('We did not find the employee you\'re looking for');
  return resp;
};

var singleEmployee = function(employee) {
  var resp = new MessagingResponse();
  var message = resp.message();
  message.body(`${employee.fullName}\n${employee.phoneNumber}\n${employee.email}`);
  message.media(employee.imageUrl);
  return resp;
};

var multipleEmployees = function(employees) {
  var resp = new MessagingResponse();
  var optionsMessage = _.reduce(employees, function(memo, it) {
    return memo += `\n${it.option} for ${it.fullName}`;
  }, '');

  resp.message(`We found multiple people, reply with:${optionsMessage}\nOr start over`);
  return resp;
};

module.exports.notFound = notFound;

module.exports.singleEmployee = singleEmployee;

module.exports.multipleEmployees = multipleEmployees;
'use strict';

var express = require('express')
  , router = express.Router()
  , twilio = require('twilio')
  , employeeFinder = require('../lib/employee-finder')
  , _ =  require('underscore')
  , twimlGenerator = require('../lib/twiml-generator');

// POST /directory/search/
router.post('/search/', function(req, res, next) {
  var body = req.body.Body;
  res.type('text/xml');

  if (req.cookies.cachedEmployees !== undefined && !isNaN(body)) {
    var cachedEmployees = req.cookies.cachedEmployees;
    var employeeId = cachedEmployees[body];
    if (employeeId === undefined) {
      res.send(twimlGenerator.notFound().toString());
    } else {
      employeeFinder.findById(employeeId, function(err, employee) {
        res.clearCookie('cachedEmployees');
        res.send(twimlGenerator.singleEmployee(employee).toString());
      });
    }
  } else {
    employeeFinder.findByName(body, function(err, employees) {
      if (employees.length === 0) {
        res.send(twimlGenerator.notFound().toString());
      } else if (employees.length === 1) {
        res.send(twimlGenerator.singleEmployee(employees[0]).toString());
      } else {
        var options = _.map(employees, function(it, index) {
          return { option: index + 1, fullName: it.fullName, id: it.id };
        });
        var cachedEmployees = _.object(_.map(options, function(it) { return [it.option, it.id]; }));
        res.cookie('cachedEmployees', cachedEmployees, { maxAge: 1000 * 60 * 60 });

        res.send(twimlGenerator.multipleEmployees(options).toString());
      }
    });
  }
});

module.exports = router;
'use strict';

var express = require('express')
  , router = express.Router()
  , twilio = require('twilio')
  , employeeFinder = require('../lib/employee-finder')
  , _ =  require('underscore')
  , twimlGenerator = require('../lib/twiml-generator');

// POST /directory/search/
router.post('/search/', function(req, res, next) {
  var body = req.body.Body;
  res.type('text/xml');

  if (req.cookies.cachedEmployees !== undefined && !isNaN(body)) {
    var cachedEmployees = req.cookies.cachedEmployees;
    var employeeId = cachedEmployees[body];
    if (employeeId === undefined) {
      res.send(twimlGenerator.notFound().toString());
    } else {
      employeeFinder.findById(employeeId, function(err, employee) {
        res.clearCookie('cachedEmployees');
        res.send(twimlGenerator.singleEmployee(employee).toString());
      });
    }
  } else {
    employeeFinder.findByName(body, function(err, employees) {
      if (employees.length === 0) {
        res.send(twimlGenerator.notFound().toString());
      } else if (employees.length === 1) {
        res.send(twimlGenerator.singleEmployee(employees[0]).toString());
      } else {
        var options = _.map(employees, function(it, index) {
          return { option: index + 1, fullName: it.fullName, id: it.id };
        });
        var cachedEmployees = _.object(_.map(options, function(it) { return [it.option, it.id]; }));
        res.cookie('cachedEmployees', cachedEmployees, { maxAge: 1000 * 60 * 60 });

        res.send(twimlGenerator.multipleEmployees(options).toString());
      }
    });
  }
});

module.exports = router;