Twitter GeoTrending app using Node.js and AngularJS
Older Article
This article was published 13 years ago. Some information may be outdated or no longer applicable.
I’ve been using Twitter more and more recently, so I figured I’d poke at their REST API and see what falls out. After some thinking, I built an app that pulls trending topics near a user’s location through a series of geo lookups.
The app lives on GitHub. Follow the setup instructions to run it yourself.
Let’s walk through the code. There’s plenty happening under the hood.
The app has two parts (invisible to the user): a backend that talks to Twitter’s REST endpoints, and a frontend that takes the returned data and renders it.
First things first: familiarise yourself with the Twitter APIs, especially the REST API. Once you’ve got your bearings, create your first Twitter application. You’ll need it for authentication. (Version 1.1 of the API only accepts authenticated calls, which was a big shift from v1.)
Here’s the process flow:
- Grab the user’s location via HTML5 Geolocation (wrapped in an AngularJS Factory)
- Hit Yahoo’s Geo API lookup service. We need this because the trends endpoint expects a Yahoo! Where On Earth ID (Yahoo’s GeoPlanet API has since been deprecated)
- If a city name maps to multiple locations (Herndon exists in Virginia, Pennsylvania, Kansas, Kentucky, and West Virginia), the user picks the right one
- Once we’ve got the ID, we call Twitter’s trends service
- If trends exist, they get listed. If not, the user can try a different city
Let’s start with the frontend: HTML5 and AngularJS. All the interesting work happens in Angular, so here’s the geolocation factory:
'use strict';
angular.module('geolocation', []).constant('geolocation_msgs', {
'errors.location.unsupportedBrowser':
'Browser does not support location services',
'errors.location.notFound': 'Unable to determine your location',
});
angular.module('geolocation').factory('geolocation', [
'$q',
'$rootScope',
'$window',
'geolocation_msgs',
function ($q, $rootScope, $window, geolocation_msgs) {
return {
getLocation: function () {
var deferred = $q.defer();
if ($window.navigator && $window.navigator.geolocation) {
$window.navigator.geolocation.getCurrentPosition(
function (position) {
$rootScope.$apply(function () {
deferred.resolve(position);
});
},
function (error) {
$rootScope.$broadcast(
'error',
geolocation_msgs['errors.location.notFound']
);
$rootScope.$apply(function () {
deferred.reject(geolocation_msgs['errors.location.notFound']);
});
}
);
} else {
$rootScope.$broadcast(
'error',
geolocation_msgs['errors.location.unsupportedBrowser']
);
$rootScope.$apply(function () {
deferred.reject(
geolocation_msgs['errors.location.unsupportedBrowser']
);
});
}
return deferred.promise;
},
};
},
]);
(The original code was forked from GitHub. That version doesn’t work with AngularJS v1.2.4. I’ve updated it and it’s available here.)
This factory hands back latitude and longitude for the user (assuming their browser supports HTML5 Geolocation and they’ve enabled Location Services).
The factory gets injected into the main app.js:
angular.module('twitter-geotrend', ['geolocation']);
Once the location’s been detected, it feeds into the first Yahoo API call:
geolocation.getLocation().then(function(data) {
$http.get('http://query.yahooapis.com/v1/public/yql?q=select%20*%20from%20geo.placefinder%20where%20text%3D%22'+data.coords.latitude+'%2C'+data.coords.longitude+'%22%20and%20gflags%3D%22R%22&format=json').success(function(data) {
$scope.city = data.query.results.Result.city
$scope.detected = true;
})
This populates the city model and enables the right form controls.
To fetch the trends, the getTrends() function fires. It does two things.
First, it looks up the “Where On Earth ID” through Yahoo’s service. Multiple results get shown to the user:
$http.get('http://query.yahooapis.com/v1/public/yql?q=select%20*%20from%20geo.placefinder%20where%20text=%22'+$scope.city+'%22&format=json')
.success(function(data) {
if (Object.prototype.toString.call(data.query.results.Result) === '[object Array]') {
$scope.detected = false;
$scope.multicity = true;
$scope.cities = data.query.results.Result;
}
Second, if only one result comes back, it calls the backend service with the retrieved ID:
else {
$scope.woeid = data.query.results.Result.woeid;
$scope.detected = true;
woeid = $scope.woeid;
$http.get('/api/trends/' + woeid)
.success(function(data, status, headers, config) {
if (data.status) {
$scope.trends = data.trends[0].trends;
} else {
$scope.errorMsg = "Can't find trends around " + $scope.city + ". Try another search.";
$scope.error = 1;
}
});
}
});
Now for the backend.
I originally wanted to build the whole thing in AngularJS alone, but then I realised I’d have to store my consumer and access tokens somewhere. Stuffing them into client-side code would’ve been a terrible idea. So I spun up a separate Node.js service to hold those details and talk to Twitter.
Like most of my projects, it runs on ExpressJS with these routes:
app.get('/', routes.index);
app.get('/api/trends/:woeid', api.trends);
The first route serves the HTML page. The second handles the Twitter API calls using a brilliant npm library called ‘twit’:
var Twit = require('twit'),
config = require('../config'),
T = new Twit(config);
exports.trends = function (req, res) {
var woeid = req.params.woeid;
T.get('trends/place', { id: woeid }, function (err, data) {
if (typeof data === 'undefined') {
res.json({ status: false });
} else {
res.json({ trends: data, status: true });
}
});
};
Per the setup instructions, don’t forget to add a config.js file to the project root with your consumer and access tokens:
module.exports = {
consumer_key: 'xxx',
consumer_secret: 'xxx',
access_token: 'xxx',
access_token_secret: 'xxx',
};
The rest is fairly straightforward. If the Twitter REST call returns data (not undefined), it gets passed to AngularJS, which picks it up via $http.get() and assigns it to the right $scope variable.
That $scope variable then gets rendered in index.html:
<ul class="list-unstyled">
<li ng-repeat="trend in trends"><a target="_blank" href="{{ trend.url }}">{{trend.name}}</li>
</ul>
Here are a few screenshots of the app in action:
Thanks for reading. Hope you found something useful here.