I decided to build a travel web app this morning. I had been looking at the Sabre Developer Studio yesterday, and I wanted to get my hands dirty. I had also been looking at LumX a few days ago, an AngularJS web frontend framework that implemented the Google Material Design guidelines, and wanted to give it a spin.
Follow along to see how I did it - all the way from coding to deployment on EC2, built using ExpressJS & Angular.
destination and theme, give search result
put it on AWS and connect to database we just builded
API interface
Then I cranked up my code editor and wrote some sample code to test the API's out, mostly the same stuff that was there on Sabre's GitHub account:
var SabreDevStudio = require('sabre-dev-studio');
var sabre_dev_studio = new SabreDevStudio({
client_id: '<your client id here>',
client_secret: '<your client secret here>',
uri: 'https://api.test.sabre.com'
});
var options = {};
var callback = function(error, data) {
if (error) {
console.log(error);
} else {
console.log(JSON.stringify(JSON.parse(data)));
}
};
sabre_dev_studio.get('/v1/lists/supported/cities', options, callback);
sabre_dev_studio.get('/v1/shop/flights/fares?origin=NYC&departuredate=2015-05-25&returndate=2015-05-30&maxfare=200', options, callback);
Running this was useful, as it gave me the idea of the output I'd get. For the /cities API call, the output was like this:
{"Cities":[
{"code":"AUH","name":"Abu Dhabi","countryCode":"AE","countryName":"United Arab Emirates","regionName":"Middle East","Links":[{"rel":"airportsInCity","href":"https://api.test.sabre.com/v1/lists/supported/cities/AUH/airports"}]},
{"code":"ALY","name":"Alexandria","countryCode":"EG","countryName":"Egypt","regionName":"Middle East","Links":[{"rel":"airportsInCity","href":"https://api.test.sabre.com/v1/lists/supported/cities/ALY/airports"}]}]}
For the /fares API call, the output was like this:
{"OriginLocation":"NYC","FareInfo":[{"CurrencyCode":"USD","LowestNonStopFare":136.2,"LowestFare":136.2,"DestinationLocation":"BUF","DepartureDateTime":"2015-05-25T00:00:00","ReturnDateTime":"2015-05-30T00:00:00","Links":[{"rel":"shop","href":"https://api.test.sabre.com/v1/shop/flights?origin=NYC&destination=BUF&departuredate=2015-05-25&returndate=2015-05-30&pointofsalecountry=US"}]}]}
Brilliant - so, I could now get a list of the airport codes & city names to show as starting cities & I could also get the list of fares & destinations back... the rest of this was going to be a piece of cake!
Time now: 8:40 am, 80 minutes to go...
I spent some time wondering if I should use Browserify on the Sabre npm module, and use it that way. But then I didn't know enough about Browserify, so I said, what the heck, lets just hack up a server real quick.
First, I created the directory structure, fairly simple, and touched a couple of files, package.json and app.js:
- sabrelab
--- config
--- www
--- package.json
--- app.js
Inside package.json, I put in some standard stuff that I use in every project, and also included sabre-dev-studio. My package.json looked like this:
{
"dependencies": {
"body-parser": "latest",
"compression": "latest",
"cookie-parser": "latest",
"dateformat": "~1.0.7-1.2.3",
"errorhandler": "latest",
"express": "latest",
"express-session": "latest",
"http-post": "~0.1.1",
"memory-cache": "0.0.5",
"method-override": "latest",
"morgan": "latest",
"mstring": "^0.1.2",
"multer": "^0.1.0",
"serve-favicon": "latest",
"string": "~1.8.0",
"underscore": "latest",
"validator": "^3.19.1",
"sabre-dev-studio": "^1.0.1"
},
"name": "sabrelab",
"subdomain": "sabrelab",
"scripts": {
"start": "node app.js"
},
"version": "0.0.1",
"engines": {
"node": "0.8.x"
}
}
I wasn't sure I needed everything there, but then, I was working against time...
Next, I whipped out app.js, mostly using what I'd used on a previous project:
(function () {
'use strict';
var express = require('express');
var app = express();
require('./config/config_app')(app);
require('./config/config_routes')(app);
// START THE SERVER
console.log('STARTING THE SABRE SERVER');
console.log('-------------------------');
app.listen(3000);
console.log('Started the server');
process.on('uncaughtException', function (error) {
console.log(error.stack);
console.log(error);
});
})();
Now, to the config_app.js in the config directory, again, mostly boilerplate stuff from a different project:
'use strict';
var express = require('express'),
bodyParser = require('body-parser'),
methodOverride = require('method-override'),
morgan = require('morgan'),
cookieParser = require('cookie-parser'),
compression = require('compression'),
errorhandler = require('errorhandler'),
multer = require('multer');
module.exports = function(app) {
app.use(compression());
app.use(morgan('dev'));
app.use(bodyParser());
app.use(multer());
app.use(cookieParser('Twyst_2014_Sessions'));
app.use(methodOverride());
app.use(express.static(__dirname + '/../www/'));
app.all("/api/*", function(req, res, next) {
res.header("Access-Control-Allow-Origin", "*");
res.header("Access-Control-Allow-Headers", "Cache-Control, Pragma, Origin, Authorization, Content-Type, X-Requested-With, Accept");
res.header("Access-Control-Allow-Methods", "GET, PUT, POST, HEAD, DELETE, OPTIONS");
return next();
});
app.use(errorhandler({
dumpExceptions: true,
showStack: true
}));
};
Now that I had the skeleton of a working server, I had to put in the routes, which is what I did next in config_routes.js. This was all new work, so it required some time and thinking:
This code came first - to initialise the Sabre Dev Studio & authorize against it using the API key:
'use strict';
var SabreDevStudio = require('sabre-dev-studio');
var sabreDevStudio = new SabreDevStudio({
client_id: '<your client_id>',
client_secret: '<your client_secret>',
uri: 'https://api.test.sabre.com'
});
var options = {};
This code came next, two small helper functions: the first one (sabreCall) to make a GET call against Sabre, and the next one (response) to send back the response to the HTTP client with either the data returned, or the error:
function sabreCall(q, res) {
sabreDevStudio.get(q, options, function(err, data) {
response(res, err, data);
});
}
function response(res, err, data) {
if (err) {
res.status(200).send({
'status': false,
'message': 'Error',
'info': err
});
} else {
res.status(200).send({
'status': true,
'message': 'Success',
'info': data
});
}
}
All this work meant that setting up the API's was going to be trivial - just calling our sabreCall function with the API's we need:
module.exports = function(app) {
app.get('/api/v1/cities', function(req,res) {
sabreCall('/v1/lists/supported/cities', res);
});
app.get('/api/v1/places', function(req,res) {
sabreCall('/v1/shop/flights/fares?origin=' + req.query.origin +
'&departuredate=' + req.query.departuredate +
'&returndate=' + req.query.returndate +
'&maxfare=' + req.query.maxfare, res);
});
};
That was it - I started up the server (below), and tried hitting the API's from Chrome by navigating to http://localhost:3000/api/v1/cities, and got back the expected response!
$ node app.js
Now all that was left was to write the front-end! I looked at my watch, and saw that I'd spent half an hour on the server, so I had 50 minutes left.
I had seen LumX a few days ago, and was intrigued by it. I wanted to try out the Google Material Design and see how it felt. At the same time, I wasn't sure I could make it in the time I had left.
Should I use Bootstrap instead? I had used it before, which was a good thing. But then, what the heck, you live only once :) LumX it was going to be.
The first nice thing about LumX was that it was really easy to install, and it also installed everything else I needed. I navigated into the www directory, and got started:
$ cd ~/Lab/sabrelab/www
$ bower install lumx
This installed LumX. Also, on the LumX site(http://ui.lumapps.com/getting-started/installation), they have a great getting started that asked me to add this to my index.html. So, I started up a www/index.html file, and put this into it:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Get Lost</title>
<link rel="stylesheet" href="bower_components/lumx/dist/lumx.css">
<link rel="stylesheet" href="bower_components/mdi/materialdesignicons.css">
<link rel="stylesheet" href="http://fonts.googleapis.com/css?family=Roboto:300,400,500,700">
</head>
<body ng-app="getLostApp">
<script src="bower_components/jquery/dist/jquery.js"></script>
<script src="bower_components/velocity/velocity.js"></script>
<script src="bower_components/moment/min/moment-with-locales.js"></script>
<script src="bower_components/angular/angular.js"></script>
<script src="bower_components/lumx/dist/lumx.js"></script>
<script src="./app.js"></script>
</body>
</html>
I then created an app.js file (referenced on the last line of the HTML file):
angular.module('getLostApp', ['lumx']).
controller('MainCtrl', function($rootScope, $scope, $http) {
// Get the cities data that I can show in the drop-down
$http.get('/api/v1/cities').success(function(data) {
$scope.cities = (JSON.parse(data.info)).Cities;
console.log($scope.cities);
}).error(function(err) {
$scope.error = err;
});
// Set some prices that I can show in the prices drop-down
$scope.prices = [
{show:'$200', value:200},
{show:'$300', value:300},
{show:'$400', value:400},
{show: '$500', value:500}
];
// Initialize this with what to show when the page is loaded
$scope.info = {
origin: {
name: 'New York City',
code: 'NYC'
},
maxfare: {
show: '$500',
value: 500
},
returndate: '2015-05-20',
departuredate: '2015-05-15'
};
// Call the server to get the fares info
$scope.submit = function() {
$http.get('/api/v1/places?origin=' + $scope.info.origin.code +
'&departuredate=' + formatDate($scope.info.departuredate) +
'&returndate=' + formatDate($scope.info.returndate) +
'&maxfare=' + $scope.info.maxfare.value).success(function(data) {
$scope.results = data;
$scope.data = data.info;
if ($scope.results.status) {
$scope.fareinfo = JSON.parse($scope.data).FareInfo;
} else {
$scope.error = JSON.parse($scope.data.data).message;
}
}).error(function(err) {
$scope.error = JSON.parse(err.data).message;
});
};
// Helper function from stackoverflow so that I can format the date before sending to the server
function formatDate(date) {
var d = new Date(date),
month = '' + (d.getMonth() + 1),
day = '' + d.getDate(),
year = d.getFullYear();
if (month.length < 2) {
month = '0' + month;
}
if (day.length < 2) {
day = '0' + day;
}
return [year, month, day].join('-');
}
});
Now, I all I had to do was the UI using LumX, and then hook it up with some simple AngularJS to call my functions. But this is where I got stuck: I'm not great at building the front-end UI, and the LumX documentation wasn't great, especially on how to use the FlexBox (maybe I dont know enough about FlexBox?).
Anyway, not much time to think - it's time to code up this UI! What did I want, let me think about this:
- A heading
- A row with two colums: on to select the start city, and the other to put in my budget.
- A row with a start and end date
- A submit button
- A place to show the results - error or success
Taking it one at a time, this is how the UI built up: first the heading:
<div class="p+">
<span class="fs-display-3 display-block">Get Lost</span>
<span class="fs-title display-block">goofing around with @appa</span>
</div>
Then, the reference to the MainCtrl and the row...
<div ng-controller="MainCtrl">
<div class="mt++" flex-container="row" flex-gutter="24">
Next, the first column - this one would ask the user to pick a city...
<div flex-item>
<lx-select ng-model="info.origin" placeholder="Starting airport" choices="cities" floating-label>
<lx-select-selected>
{{ $selected.name }} - {{ $selected.code }}
</lx-select-selected>
<lx-select-choices>
{{ $choice.name }} - {{ $choice.code }}
</lx-select-choices>
</lx-select>
</div>
And, the next column, and end the row:
<div flex-item>
<lx-select ng-model="info.maxfare" placeholder="Budget" choices="prices" floating-label>
<lx-select-selected>
{{ $selected.show }}
</lx-select-selected>
<lx-select-choices>
{{ $choice.show }}
</lx-select-choices>
</lx-select>
</div>
</div>
Now, for the next row with the two columns with date pickers:
<div class="mt++" flex-container="row" flex-gutter="24">
<div flex-item>
<form>
<lx-date-picker model="info.departuredate" label="Start date" locale="en" fixed-label="true" icon="calendar"></lx-date-picker>
</form>
</div>
<div flex-item>
<form>
<lx-date-picker model="info.returndate" label="End date" locale="en" fixed-label="true" icon="calendar"></lx-date-picker>
</form>
</div>
</div>
Then, a row with the submit button:
<div class="mt++" flex-container="row" flex-gutter="24">
<div flex-item>
<div flex-container="column" flex-align="space-between center">
<button class="btn btn--m btn--blue btn--raised" lx-ripple ng-click="submit()">Submit</button>
</div>
</div>
</div>
Now, for the row that handles the response...
<div class="mt++" flex-container="row" flex-gutter="24" flex-wrap ng-show="results">
<div ng-show="results.status" class="p++">
<div flex-item="6">
<span class="fs-title display-block mb">Places you can go...</span>
<div class="divider divider--dark"></div>
<ul class="list mt++">
<div ng-repeat="d in fareinfo | orderBy: 'LowestFare'">
<li class="list-row list-row--has-separator">
<div class="list-row__content">
<span>{{d.LowestFare}}, {{d.DestinationLocation}}</span>
</div>
</li>
</div>
</ul>
</div>
</div>
<div ng-hide="results.status" class="p++">
<h2>Error</h2>
{{error}}
</div>
</div>
By now, I was running out of time - I had a working app. Everything was working, but I wasn't pleased yet, because:
- The UI didn't look great. LumX had worked out well for the date picker & the drop-down list, but I couldn't get the hang of how to space things around.
- The results, when returned, didn't have the full information - it had only the airport code & the price, not the full airport name etc. It also didn't look very good :(
Here's how it was looking by now:
Anyway, on to the next step - deploying to AWS. 20 minutes to go!!
The next 20 minutes went by in a rush... first of all, I committed to git, and uploaded to GitHub:
$ cd ~/Lab/sabrelab
$ git init .
$ git add .
$ git commit -am "Initial commit for Sabre Lab"
Then I went to GitHub and created a new repo at https://github.com/arunr/sabrelab. I went back to the command line and pushed my code up to GitHub, so I could later easily get to it from AWS:
$ git remote add origin https://github.com/arunr/sabrelab.git
$ git push origin master
I then went over to AWS, and created a micro-instance of Ubuntu. After downloading the PEM file, I logged in to the instance, and installed node, git and forever (so that I could keep my server running):
$ ssh -i sabrelab.pem ubuntu@<your server IP here>
Then on the Ubuntu AWS instance:
$ curl -sL https://deb.nodesource.com/setup | sudo bash -
$ sudo apt-get install -y nodejs
$ sudo apt-get install git
$ git clone https://github.com/arunr/sabrelab.git
$ sudo npm install -g forever
$ cd ~/sabrelab
$ sudo forever start app.js
After this, I had to do two things, which I had forgotten:
- Change the port in app.js from 3000 to 80.
- Add my EC2 instance to a security group that allowed for inbound access to the server on HTTP port 80.
Once I had done this, I had my public facing server: at http://52.10.111.167.
A few thoughts to close out - just my own learnings from the morning:
- I proved to myself that it is possible to create a web app from end-to-end in 2 hours (this was important for me, because I haven't had any code flow for a while now!)
- More importantly, I managed to dabble with a few new things and see how they fit - Sabre Developer API's and LumX.
- On the Sabre Developer APIs - I found this easy to start, well documented, useful and something I might use in a project in the future. The only catch was that this is all against the Sabre Test API's and I couldn't find any documentation on the pricing of the PROD API's.
- On LumX - I found the UI beautiful in the examples, but couldn't find enough documentation to make my own front-end UI look good in the half-hour I spent with it. So, I'm not dissing it, but it definitely felt hard - maybe I just need to put in more time into it.
- Next steps:
- Clean up the UI and make it look pretty
- Show more full results, with a link to booking as well
- Connect to the Sabre PROD API's
- Use geo-location to auto-fill the start city
- Wrap the entire thing in Ionic so there is a phone app version of this