SeaCat Tutorial - Chapter 5: Using Parse.com with REST Integration (iOS)
Foreword
In last article we updated our simple Node.js host in our REST Integration example and incorporated the MongoDB storage as a part of our backend infrastructure we build in our tutorial series.
Before we focus more on updating the client side (improving our IOS app), let's cover an alternative to our MongoDB (and traditional storage) solution today. There are several options how we can store data nowadays. Many of traditional (relational) and NoSQL databases (and of course, some others ones as well) are available and we can run these solutions either on our own (install by ourselves) or subscribe the hosted ones.
As the market with Cloud Computing and Mobile devices is getting bigger, there is another specific option available. It's called Mobile Backend-As-A-Service (BAAS) [1] and it is extremely useful in situations we want to subscribe a complex backend service (alongside the core backend solution, there is usually a lot of additional functionality and statistics) and primary focus on development of client part of mobile apps for instance.
We want to show you a practical example how to work with these BAAS services. For the purpose of this tutorial we chose Parse.com (there are a few more). Despite its rich and great functionality, you still have to think about security separately. We explain practically why the combination of a backend solution (Node.js in our case) and SeaCat is a great choice to handle the security part easily.
Parse.com subscription and App initialization
Before we start with our development, we have to sign up for Parse first. We can either use Facebook, Google or Github credentials or create a completely new account. There is no fee for the basic developer option.
When we login to Parse.com, there is a very nice dashboard showing currently active apps. By clicking on Create a new App button, we can simply create a new one. In our case we called our app SeaCat, but in fact, it really doesn't matter what name we use.
If we open the app details, a very nice dashboard will appear. The important part in our case is App Settings tab and Keys section. Copy Application ID and Javascript Key for the purpose of our development.
Node.js host
The code below is the extension of the basic structure developed in the previous part of our tutorial. The main focus is related to data handling and Parse.com utilisation.
Standard security concerns
In our solution we use a NPM package based on Parse.com Javascript SDK [2]. As the standard JavaScript use-cases are related to front-end, there is a risk of leaking of sensitive information by improper implementation. This information may appear in browsers quite easily. We eliminated some potential leaks by implementing the solution in Node.js and adding SeaCat as another security layer.
Required packages
There are 3 NPM packages used in the application.
- Express - Main framework for Node.js helping to simplify the most common HTTP operations. Type
npm install express
in command line to install this package. - BodyParser - A very useful package for parsing data in requests. Type
npm install body-parser
in command line to install this package. - Parse - A npm package based on Parse.com Javascript SDK for handling the comunication between our Node.js Host and Parse.com backend. Type
npm install parse
in command line to install this package.
Script initialization
Once the packages are installed (in the same directory), we can start with our development and simply incorporate them in our script. There will be several chunks of code with some additional explanation in this text. However, the result will be 1 script (NodeRESTHostWithParse.js in SeaCat GitHub). The initial part looks like following (don't forget to fill the security information to APP_ID and JAVASCRIPT_KEY variables according to your Parse.com settings).
var Express = require('express');
var BodyParser = require('body-parser');
var Parse = require('parse').Parse;
var APP_ID = "YOUR_APP_ID";
var JAVASCRIPT_KEY = "YOUR_JAVASCRIPT_KEY";
// Connect with Parse.com Infrastructure.
var parseApp = Parse.initialize(APP_ID, JAVASCRIPT_KEY);
// Initialize app by using Express framework.
var app = Express();
// Use Body Parser (Helps to process incoming requests).
app.use(BodyParser.urlencoded({extended: true}));
app.use(BodyParser.json());
// Set default port 1337 or custom if defined by user externally.
app.set('port', process.env.PORT || 1337);
// Configure variables related to Parse.com for working with our Movies schema
var Movies = Parse.Object.extend("Movies");
var query = new Parse.Query(Movies);
// Simple function which checks whether a parameter is null or not.
function isNull(parameter) {
return parameter === null ? true : false;
}
Initial data
It's generally good idea to provide some initial data. Following piece of code contains the same three initial data objects like in our last application and load it to Parse.com backend if application is launched for the first time. Othewise the code is ignored.
// Seeding Initial Data. Populates only once, then the code is skipped.
query.find({
success: function(movies) {
if (movies.length) {
return;
}
// Initial array with some movies.
var movies = [
{mid: 1, name: "Forrest Gump", director: "Robert Zemeckis", release: 1994},
{mid: 2, name: "Donnie Darko", director: "Richard Kelly", release: 2001},
{mid: 3, name: "Inception", director: "Christopher Nolan", release: 2010}
];
for (var i = 0; i < movies.length; i++) {
// Create a new object which represents Parse.com object.
var movie = new Movies();
// Set values from array.
movie.set("mid", movies[i].mid);
movie.set("name", movies[i].name);
movie.set("director", movies[i].director);
movie.set("release", movies[i].release);
// Save object to backend (Parse.com).
movie.save(null, {
success: function(movie) {
// Execute any logic that should take place after the object is saved.
console.log('New object created with objectId: ' + movie.id);
},
error: function(movie, error) {
// Execute any logic that should take place if the save fails.
// error is a Parse.Error with an error code and message.
console.log('Failed to create new object, with error code: ' + error.message);
}
});
}
},
error: function(object, error) {
// The object was not retrieved successfully.
// error is a Parse.Error with an error code and message.
console.log(error);
}
});
REST methods
The interface related to REST API methods is the same as in our original example covered in our REST integration article. We still consider following:
- GET /api/movies - get a list of all movies stored in memory.
- GET /api/movies/:id - get detail of particular movie using its id.
- POST /api/movies - create a new movie entry.
- PUT /api/movies/:id - update an existing movie entry.
- DELETE /api/movies/:id - delete and existing movie entry.
Method helps to GET all records. The basic idea is to read each record from Parse.com object (find method with no filter) and return the response in JSON format.
// GET - list of all records.
app.get('/api/movies', function(request, response, next){
var localQuery = new Parse.Query(Movies);
// Find all records stored in Movies Object.
localQuery.find({
success: function(results) {
// List of all records from Parse.com backend and return as a JSON.
response.json(results.map(function(movie){
return {
id: movie.attributes.mid,
name: movie.attributes.name,
director: movie.attributes.director,
release: movie.attributes.release
}
}));
},
// In case of any error, forward request to error handler.
error: function(error) {
console.log("Error: " + error.code + " " + error.message);
next();
}
});
});
Method helps to GET record based on ID. If not found, forward the request to 404 - not found. Added aditional checking if the specified ID is in integer format. ID lookup is based on 'mid' attribute (abbreviation for movie id), which is unique for each record in our Parse.com dataset.
// GET - list of a record with particular id. If not found, forward the request to 404 - not found.
app.get('/api/movies/:id', function(request, response, next){
var localQuery = new Parse.Query(Movies);
// Read ID and parse its integer value from URL request.
var id = parseInt(request.params.id);
// Find the particular object.
localQuery.equalTo("mid", id);
localQuery.find({
success: function(movie) {
// In case of empty result, forward to 404.
if (!movie.length) {
next();
}
else {
// return as a JSON with the result. Result is returned in form of array with 1 element.
response.json({
id: movie[0].attributes.mid,
name: movie[0].attributes.name,
director: movie[0].attributes.director,
release: movie[0].attributes.release
});
}
},
// In case of any error, forward request to error handler.
error: function(error) {
console.log("Error: " + error.code + " " + error.message);
next();
}
});
});
Method helps to CREATE a new record. Automatically increase the ID ('mid' attribute) value. We added this field due to compatibility with our existing solution. Represents the primary key and we use this field for lookup purposes.
// POST - create a new element.
app.post('/api/movies', function(request, response, next){
var localQuery = new Parse.Query(Movies);
// Complete request body.
var requestBody = request.body;
// Get number of elements stored in Backend, to be able to increase the custom ID.
localQuery.find({
success: function(results) {
// Read number of elements.
var numberOfElements = results.length
// Prepare new data.
var newMovie = {
mid: ++numberOfElements,
name: requestBody.name,
director: requestBody.director,
release: requestBody.release
};
// Create a new object.
var movie = new Movies();
// Set values from array.
movie.set("mid", newMovie.mid);
movie.set("name", newMovie.name);
movie.set("director", newMovie.director);
movie.set("release", newMovie.release);
// Save object to backend (Parse.com).
movie.save(null, {
success: function(movie) {
// Execute any logic that should take place after the object is saved.
console.log('New object created with objectId: ' + movie.id);
// Prepare status code 200 and close the response.
response.status(200).end();
},
error: function(movie, error) {
// Execute any logic that should take place if the save fails.
// error is a Parse.Error with an error code and message.
console.log('Failed to create new object, with error code: ' + error.message);
next();
}
});
},
error: function(error) {
// In case of an issue forward the response to ERROR handles.
console.log("Error: " + error.code + " " + error.message);
next();
}
});
});
Method helps to UPDATE existing record based on input ID. We added functionality which made each updating field as an optional. If we don't include fields name, director or release in our request JSON, the original existing values will persist.
// PUT - update existing element.
app.put('/api/movies/:id', function(request, response, next){
var localQuery = new Parse.Query(Movies);
// Complete request body.
var requestBody = request.body;
// Read ID from user's request and parse its integer value.
var id = parseInt(request.params.id);
// Continue only when id is an actual integer number.
if (!isNaN(id)) {
localQuery.equalTo("mid", id);
localQuery.find({
success: function(results) {
// In case of empty result, forward to 404.
if (!results.length) {
next();
}
else {
// Prepare data update. If any of existing element is null in JSON, read the older values.
var movieUpdate = {
mid: id,
name: isNull(requestBody.name) ? results[0].get("name") : requestBody.name,
director: isNull(requestBody.director) ? results[0].get("director") : requestBody.director,
release: isNull(requestBody.release) ? results[0].get("release") : requestBody.release
};
// Update object.
results[0].save(null, {
success: function(movie) {
// Now let's update it with some new data.
movie.set("name", movieUpdate["name"]);
movie.set("director", movieUpdate["director"]);
movie.set("release", movieUpdate["release"]);
movie.save();
// Element successfuly updated. Prepare status code 200 and close the response.
response.status(200).end();
},
error: function(error) {
console.log("Error: " + error.code + " " + error.message);
next();
}
});
}
},
// In case of any error, forward request to error handler.
error: function(error) {
console.log("Error: " + error.code + " " + error.message);
next();
}
});
} else {
// In case of an issue forward the response to ERROR handles.
console.log("You specified invalid number!");
next();
}
});
Method helps to DELETE existing record based on ID.
// DELETE - remove particular record from array.
app.delete('/api/movies/:id', function(request, response, next){
var localQuery = new Parse.Query(Movies);
// Read ID and parse its integer value from URL request.
var id = parseInt(request.params.id);
// Continue only when id is an actual integer number.
if (!isNaN(id)) {
// Find the particular object.
localQuery.equalTo("mid", id);
// Delete record based on movie id.
localQuery.find({
success: function(results) {
// In case of empty result, forward to 404.
if (!results.length) {
next();
}
else {
// Actual record removing.
results[0].destroy({});
console.log("Deleted 1 row from Parse.com!");
// Prepare status code 200 and close the response.
response.status(200).end();
}
},
// In case of an issue forward the response to ERROR handles.
error: function(error) {
console.log("Error: " + error.code + " " + error.message);
next();
}
});
} else {
// In case of an issue with input integer number forward the response to ERROR handles.
console.log("You specified invalid number!");
next();
}
});
Script finalization
Last methods are related to error handling in case the request is invalid and start listening on defined port.
// Use Express midleware to handle 404 and 500 error states.
app.use(function(request, response){
// Set status 404 if none of above routes processed incoming request.
response.status(404);
// Generate the output.
response.send('404 - not found');
});
// 500 error handling. This will be handled in case of any internal issue on the host side.
app.use(function(err, request, response){
// Set response type to application/json.
response.type('application/json');
// Set response status to 500 (error code for internal server error).
response.status(500);
// Generate the output - an Internal server error message.
response.send('500 - internal server error');
});
// Start listening on defined port, this keep running the application until hit Ctrl + C key combination.
app.listen(app.get('port'), function(){
console.log("Host is running and listening on http://localhost:" + app.get('port') + '; press Ctrl-C to terminate.');
});
The complete script in our GIT repository
Running the scripts
Once the initial implementation is done, it is time to configure the environment and keep the scripts up and running.
Running Node.js host
To run Node.js script is also very straightforward. Just open the command line and type node NodeRESTHostWithParse.js
(in case your script has different name, just replace the NodeRESTHostWithParse.js parameter accordingly).
Running SeaCat Gateway
SeaCat configuration is also very easy. First of all make sure that the seacat-trial.conf in your SeaCat directory contain following settings. (our host listens on port 1337).
[host:nodejshost]
uri=http://127.0.0.1:1337
Once the previous step is done, the last step which is necessary to do is to run SeaCat Gateway in command line terminal by typing ./seacatd-trial
.
iOS Client
Following section assumes you have completed the iOS application from previous tutorial. If so, just run the app in the Simulator and play around. As we didn't change the interface, your application should work without any change.
You can download the whole application from GitHub.
Conclusion
In this tutorial we basically extended the original Node.js application and replaced the MongoDB solution by Parse.com Backend-As-A-Service. With no change on iOS side we were able to utilize this change very smoothly. The next article of our tutorial series will be dedicated to final change on iOS client side.
Reference:
- http://en.wikipedia.org/wiki/Mobile_Backend_as_a_service
- https://parse.com/docs/js_guide
SeaCat iOS tutorials in this series:
Most Recent Articles
- A beginner-friendly intro to the Correlator for effective cybersecurity detection
- Inotify in ASAB Library
- From State Machine to Stateless Microservice
- Entangled ways of product development in the area of cybersecurity #3 - LogMan.io
- Entangled ways of product development in the area of cybersecurity #2 - BitSwan
You Might Be Interested in Reading These Articles
SeaCat Tutorial - Chapter 4: Using MongoDB with REST Integration (iOS)
The previous tutorial introduces several concepts and helps you understand the basic of REST API integration with iOS client written in Swift. There are several limitations, with data storage being the most important. This article provides instructions on how to work around this restriction.
Published on November 25, 2014
A beginner-friendly intro to the Correlator for effective cybersecurity detection
At TeskaLabs, we know that a cybersecurity system is only as effective as its ability to detect threats. That's why we developed a powerful tool that will prove essential in your arsenal: the Correlator.
Published on March 15, 2024
Streaming Data from Various Sources
One of the main research and development topics nowadays is data processing and analysis, which can help companies discover relevant information about their customers or technologies using reports, visualizations, dashboards, and other business intelligence outputs.
Published on June 01, 2018