CORS and writing our basic server

Software required for this section:


Before going any further, lets try an experiment. Let's see what happens if we attempt to make a basic AJAX HTTP call to Steam's Web API from a local host browser. Steam Web API calls come in a familiar format with your key(if needed) and query parameters contained in the URL:

 http://api.steampowered.com/ISteamNews/GetNewsForApp/v0002/?appid=440&count=3&maxlength=300&format=json

In the above example, we would be querying to obtain the most recent news for a game represented by its App Id (440). We would recieve the 3 most recent news updates with a max length for the news stories of 300 characters.

The Steam Web API methods can return results in 3 different formats: JSON, XML, and VDF(a Valve proprietary format). For our purposes, we will be parsing the JSON objects. So to make a basic AJAX HTTP client call to the Steam Web API, we would create a basic Javascript GET request like this:

 var req = new XMLHttpRequest();
 req.open("GET", "http://api.steampowered.com/ISteamNews/GetNewsForApp/v0002/?appid=440&count=3&maxlength=300&format=json", false);
 req.send(null);
 console.log(JSON.parse(req.responseText));

When we execute this script and we go to the console in our browser, we are presented with this error:

This is an example of Steam not allowing something called Cross Origin Resource Sharing, or CORS. So what is CORS and what do we do if Steam does not allow it?


Cross Origin Resource Sharing

When we tried to make the above call to the Steam Web API, we were making a cross-origin HTTP request. The XMLHttpRequest was originating in our script from our local host domain and making the request to Steam's domain. For security purposes, modern browsers restrict cross-origin HTTP requests. In other words, our XMLHttpRequest can only make calls to our own domain. Without going too in depth (and because I am still learning about CORS and therefore not entirely comfortable with the concept myself), CORS is an industry standard that gives servers cross-domain access controls that enable cross-domain communication using new HTTP headers. The problem for us in dealing with the Steam Web API is that Steam does not allow CORS requests. So we need to find a way around this.



Making Server Side HTTP calls

To get around the issue of cross-domain access, we will create our own server to make HTTP requests. We can then send our own client side requests to our server to parse and work with the return JSON objects. Let's begin by creating a new Javascript file. There are a few key lines of code we will need in this file at the beginning to get started. First, we need to load the modules we will be using with Node.js. These are Request and Express. We are also going to call the express function to return a new Express application. This should look like this:

var express = require('express');
var app = express();
var request = require('request');

Next we need to set which port our local server will be listening through for our requests:

app.set('port', 3000);

Now for the centerpiece of our server code, a route. This route will map a URL to a function that sends a request to the Steam Web API:

app.get('/getnews', function(req, res) {
var qParams = [];
for (var p in req.query) {
	qParams.push({'name':p, 'value':req.query[p]})
	}
	var url = 'http://api.steampowered.com/ISteamNews/GetNewsForApp/v0002/?appid=' + qParams[0].name + '&count=3&maxlength=300&format=json';
	request(url, function(err, response, body) {
		if(!err && response.statusCode < 400) {
			console.log(body);
			res.send(body);
		}
	});	
}); 

In this example, we are working with the same Get News method. In this route, there are 2 major parts: A part where we obtain parameters and the part where we actually send the request. The route begins with app.get indicating this will be a GET request. The get() method is passed an address and an event handler function that runs only at the designated URL (here it is "/getnews"). The first part of this route is the obtaining of parameters from the URL address request send by the client. The request object has a query property that identifies name/value pairs for all the data sent in the query string (in this case our URL). We can use this property to store these values in an array to be accessed later to create our URL for the request sent to the Steam Web API:

var qParams = [];
for (var p in req.query) {
	qParams.push({'name':p, 'value':req.query[p]})
	}

There are probably more straightforward and more elegant ways in which to obtain user input to pass on in our server side request, but at my current level of understanding, this was the easiest method I could come up with. In the above code, we are storing the query parameters in an array called qParams. For the purposes of this tutorial, we will be using primarily the AppId or SteamID number corresponding to a game or player as the parameter. In the cases where a call requires an AppId and SteamID, the URL will take the form /?AppId=SteamId So as an example, in order to send a request to return the news for the PC game Team Fortress 2 (AppId = 440), our URL would look like this: http://localhost:3000/getnews/?440


If we need to make a call that requires an AppId AND PlayerID, our URL would look something like this: http://localhost:3000//getuserstats/?440=12345

The next section of our route is the request sent to the Steam Web API. We use this portion of the request:

http://api.steampowered.com/ISteamNews/GetNewsForApp/v0002/?appid=' + qParams[0].name + '&count=3&maxlength=300&format=json

to join our parameter to the API call. Since we will always be using only one or two numbers (AppId and/or PlayerId), we will always be using the first element of the array. If we are making a call for a method that requires both the name and value of our query parameter, the call would look something like this:

http://api.steampowered.com/ISteamUserStats/GetUserStatsForGame/v0002/?appid=' + qParams[0].name + '&key=XXXXXXXXXXXXXXXXX&steamid=' + qParams[0].value

What about making cross-domain requests to our server?


In order to be able to access our server from our client browser, we need to add the necessary headers for allowing CORS. This is done by adding the following code to the beginning of our server file:

app.use(function(req, res, next) {
  res.header("Access-Control-Allow-Origin", "*");
  res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
  next();
});

Our finished server Javascript file


Now that we have figured out what our routes will look like, we can add each one to our server Javascript file so each can be appropriately called. Visit the Steam Web API documentation to find more details on the various methods. Here is what the finished file should look like (You will need to edit in your Steam API Key where appropriate):

var express = require('express');

var app = express();
var request = require('request');

app.set('port', 3000);

app.use(function(req, res, next) {
  res.header("Access-Control-Allow-Origin", "*");
  res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
  next();
});

// Here gets parameters from end of URL to use in api address.  These parameters will come from submit buttons 
// on the respective sites
app.get('/getnews', function(req, res) {
	var qParams = [];
	for (var p in req.query) {
		qParams.push({'name':p, 'value':req.query[p]})
	}
	var url = 'http://api.steampowered.com/ISteamNews/GetNewsForApp/v0002/?appid=' + qParams[0].name + '&count=3&maxlength=300&format=json';
	request(url, function(err, response, body) {
		if(!err && response.statusCode < 400) {
			console.log(body);
			res.send(body);
		}
	});	
});

app.get('/getachievementsperc', function(req, res) {
	var qParams = [];
	for (var p in req.query) {
		qParams.push({'name':p, 'value':req.query[p]})
	}
	var url = 'http://api.steampowered.com/ISteamUserStats/GetGlobalAchievementPercentagesForApp/v0002/?gameid=' + qParams[0].name + '&format=json';
	request(url, function(err, response, body) {
		if(!err && response.statusCode < 400) {
			console.log(body);
			res.send(body);
		}
	});	
});

//Requires AppId and name of achievement
app.get('/getglobalstats', function(req, res) {
	var qParams = [];
	for (var p in req.query) {
		qParams.push({'name':p, 'value':req.query[p]})
	}
	var url = ' http://api.steampowered.com/ISteamUserStats/GetGlobalStatsForGame/v0001/?format=json&appid=' + qParams[0].name + '&count=1&name[0]=' + qParams[0].value;
	request(url, function(err, response, body) {
		if(!err && response.statusCode < 400) {
			console.log(body);
			res.send(body);
		}
	});	
});

app.get('/getplayersummary', function(req, res) {
	var qParams = [];
	for (var p in req.query) {
		qParams.push({'name':p, 'value':req.query[p]})
	}
var url = 'http://api.steampowered.com/ISteamUser/GetPlayerSummaries/v0002/?key=XXXXXXXXXXXXXXXXX&steamids=' + qParams[0].name;
	request(url, function(err, response, body) {
		if(!err && response.statusCode < 400) {
			console.log(body);
			res.send(body);
		}
	});	
});

app.get('/getfriendlist', function(req, res) {
	var qParams = [];
	for (var p in req.query) {
		qParams.push({'name':p, 'value':req.query[p]})
	}
var url = 'http://api.steampowered.com/ISteamUser/GetFriendList/v0001/?key=XXXXXXXXXXXXXXXXX&steamid=' + qParams[0].name + '&relationship=friend';
	request(url, function(err, response, body) {
		if(!err && response.statusCode < 400) {
			console.log(body);
			res.send(body);
		}
	});	
});

app.get('/getplayerachievements', function(req, res) {
	var qParams = [];
	for (var p in req.query) {
		qParams.push({'name':p, 'value':req.query[p]})
	}
var url = 'http://api.steampowered.com/ISteamUserStats/GetPlayerAchievements/v0001/?appid=440&key=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX&steamid=' + qParams[0].name;
	request(url, function(err, response, body) {
		if(!err && response.statusCode < 400) {
			console.log(body);
			res.send(body);
		}
	});	
});

//Requires AppId and playerID
app.get('/getuserstats', function(req, res) {
	var qParams = [];
	for (var p in req.query) {
		qParams.push({'name':p, 'value':req.query[p]})
	}
var url = 'http://api.steampowered.com/ISteamUserStats/GetUserStatsForGame/v0002/?appid=' + qParams[0].name + '&key=XXXXXXXXXXXXXXXXX&steamid=' + qParams[0].value;
	request(url, function(err, response, body) {
		if(!err && response.statusCode < 400) {
			console.log(body);
			res.send(body);
		}
	});	
});

app.get('/getownedgames', function(req, res) {
	var qParams = [];
	for (var p in req.query) {
		qParams.push({'name':p, 'value':req.query[p]})
	}
var url = 'http://api.steampowered.com/IPlayerService/GetOwnedGames/v0001/?key=XXXXXXXXXXXXXXXXX&steamid=' + qParams[0].name + '&format=json';
	request(url, function(err, response, body) {
		if(!err && response.statusCode < 400) {
			console.log(body);
			res.send(body);
		}
	});	
});

app.get('/getrecentlyplayed', function(req, res) {
	var qParams = [];
	for (var p in req.query) {
		qParams.push({'name':p, 'value':req.query[p]})
	}
var url = 'http://api.steampowered.com/IPlayerService/GetRecentlyPlayedGames/v0001/?key=XXXXXXXXXXXXXXXXX&steamid=' + qParams[0].name + '&format=json';
	request(url, function(err, response, body) {
		if(!err && response.statusCode < 400) {
			console.log(body);
			res.send(body);
		}
	});	
});

app.get('/isplayingshared', function(req, res) {
	var qParams = [];
	for (var p in req.query) {
		qParams.push({'name':p, 'value':req.query[p]})
	}
var url = 'http://api.steampowered.com/IPlayerService/IsPlayingSharedGame/v0001/?key=XXXXXXXXXXXXXXXXX&steamid=' + qParams[0].name + '&appid_playing=' + qParams[0].value + '&format=json';
	request(url, function(err, response, body) {
		if(!err && response.statusCode < 400) {
			console.log(body);
			res.send(body);
		}
	});	
});

app.get('/getschema', function(req, res) {
	var qParams = [];
	for (var p in req.query) {
		qParams.push({'name':p, 'value':req.query[p]})
	}
var url = ' http://api.steampowered.com/ISteamUserStats/GetSchemaForGame/v2/?key=XXXXXXXXXXXXXXXXX&appid=' + qParams[0].name;
	request(url, function(err, response, body) {
		if(!err && response.statusCode < 400) {
			console.log(body);
			res.send(body);
		}
	});	
});

app.get('/getbans', function(req, res) {
	var qParams = [];
	for (var p in req.query) {
		qParams.push({'name':p, 'value':req.query[p]})
	}
var url = 'http://api.steampowered.com/ISteamUser/GetPlayerBans/v1/?key=XXXXXXXXXXXXXXXXX&steamids=' + qParams[0].name;
	request(url, function(err, response, body) {
		if(!err && response.statusCode < 400) {
			console.log(body);
			res.send(body);
		}
	});	
});

app.use(function(req,res){
  res.type('text/plain');
  res.status(404);
  res.send('404 - Not Found');
});

app.use(function(err, req, res, next){
  console.error(err.stack);
  res.type('plain/text');
  res.status(500);
  res.send('500 - Server Error');
});

app.listen(app.get('port'), function(){
  console.log('Express started on http://localhost:' + app.get('port') + '; press Ctrl-C to terminate.');
});
One last section of our server Javascript file is app.listen(). This will start our Express server listening on the indicated port, and runs a short callback message informing us that it is running.
app.listen(app.get('port'), function(){
  console.log('Express started on http://localhost:' + app.get('port') + '; press Ctrl-C to terminate.');
});

One final step is to run our server file using Node.js by typing node server.js into the Node.js console (substitute for whatever you named your .js file). If all has gone well, you should be shown a message similar to this:



Making our client side AJAX call »