There is no shortage of Node.js CRUD examples, the idea behind this 2-part tutorial series is to level-up the pool of CRUD examples to include a focus on spatial. The tutorial will cover using Node.js and the Leaflet open-source JavaScript library to Create, Read, Update and Delete (CRUD) GeoJSON line vector feature content held in a back-end MongoDB database.

The inspiration for this tutorial series comes from the Writing a CRUD app with Node.js and MongoDB tutorial by Eslam Maged. Eslam has done a great job structuring his tutorial, and in my opinion, has produced one of the nicer CRUD Nodejs tutorials out there. I did not want to re-invent the wheel but rather focus on the CRUD relationship with GeoJSON, then, in Part 2, connect the RESTful API with the front end Leaflet library.

Part 1 of the workshop will focus on building a RESTful API for our GeoJSON content. A RESTful API is an Application Program Interface (API) that uses HTTP requests to POST (Create), GET (Read), PUT (Update), and DELETE (Delete) data. The app will adopt a Models, Views and Controllers (MVC) design pattern, this approach promotes understanding and helps to clearly define tasks and enable users to separate functionality into digestible and meaningful chunks. We will use Postman, a collaboration platform for API development to test the transactions.

In Part 2 of the tutorial (coming soon), we will extend the Leaflet.js mapping library using the Leaflet.Editable.js library as a front-end editing tool to communicate with the RESTful API Create, Read, Update and Delete functionality built in Part 1 of the series.

Part 1 of the tutorial series can stand alone but is a pre-requisite for Part 2 (coming soon).

If you are looking to level up your GeoJSON CRUD skills and have fun with Node.js, Leaflet and Mongo then this might be the tutorial you are looking for.

 

Let’s get started

First, we will create a simple Node.js app, this will serve as the foundation for our Geo-CRUD app.

Create a directory called GeoApp, then inside the GeoApp directory, run the following command in the terminal:

npm init

The above command is used to set up a new npm package. Accept the default options by pressing return for each line item. Once completed, you will notice a new package.json file in your directory, this file is used to manage the locally installed Node.js (npm) packages. Open the package.json file in a text editor and take a look.

We will use the npm install command to add three additional packages that will extend Node.JS and allow us to work with MongoDB and handle JSON requests.

Make sure you are in the GeoApp directory then run the following command in the terminal:

npm install --save express body-parser mongoose

Re-Open the package.json file, notice the added dependencies of body-parser, express and mongoose. You now also have a node modules directory in your GeoApp directory.

The express framework is built on top of the node.js framework and helps in fast-tracking development of server based applications. Routes are used to divert users to different parts of the web applications based on the request made.

The body-parser module parses the JSON, buffer, string and URL encoded data submitted using HTTP POST request. this is a key requirement for our Create end point.

The mongoose module is open-sourced with the MIT license and is also maintained by MongoDB, Inc. It provides an object data modeling (ODM) environment that wraps the Node.js native driver. Mongoose's main value is that you can define schemas for your collections which are then enforced at the ODM layer by Mongoose.

 

Initializing the Server

Create a new file named app.js inside the GeoApp directory and add the following content:

// app.js
const express = require('express');
const bodyParser = require('body-parser');

// initialize the express app
const app = express();
let port = 1234;

app.listen(port, () => {
    console.log('The server is running on port number ' + port);
});

The Express and body-parser dependencies have been setup and a port number has been assigned to our app. Run the following command in the terminal:

node app.js

If you navigate to port 1234 in your browser you will see an error, this is because we have not yet setup any route or controller functions, we will do that next. You should see the following output in your terminal:

run-on-port

 

Logical File Separation Using MVC

From this point on, it makes sense to separate logic and organise our app directory into sensible subdirectories and group them based on their functionality and role. We will use the Model, View, Controller MVC design pattern to do this.

Inside the GeoApp directory, create the following subdirectories:

  1. models - this will include all the code for our database model which will define the structure of the line vector feature GeoJSON we will be storing.
  2. views - this subdirectory will be populated in part 2 of the tutorial when we add the Leaflet web map interface.
  3. controllers - the logic of how the app handles the incoming requests and outgoing responses.
  4. routes - tell the client (browser/mobile app) to go to which Controller once a specific url/path is requested.

In the models directory, create a new file called geo.model.js and add the following content:

// models/geo.model.js
const mongoose = require('mongoose');
const Schema = mongoose.Schema;

let GeoSchema = new Schema({
  type: String,
  name: String,
  style: {
    color: String,
    weight: Number,
    opacity: Number
  },
  geometry: {
    type: {
      type: String,
      enum: ['LineString'],
      required: true
    },
    coordinates: {
      type: Array,
      index: '2d'
    }
  },
  properties: {
    direction: String,
    power: Number
  }
});

// Export the model
module.exports = mongoose.model('Geo', GeoSchema);

Pay attention to the geometry type and coordinates objects, these are important components of our GeoJSON model.

Note the reference to the mongoose dependency and the schema definition for our linear vector feature model. On the final line the model is exported so it can be accessed by other application files.

In the routes directory, create a new file named geo.route.js and add the following content:

// routes/geo.route.js
const express = require('express');
const router = express.Router();

// Require the controllers
const geo_controller = require('../controllers/geo.controller');

// test url.
router.get('/test', geo_controller.test);
module.exports = router;

In the controllers directory, create a new file named geo.controller.js and add the following content:

// controllers/geo.controller.js
const Geo = require('../models/geo.model');

//Simple version, without validation or sanitation
exports.test = function (req, res) {
    res.send('hello test controller!');
};

In the app.js file, add a reference to the route class:

// app.js
const express = require('express');
const bodyParser = require('body-parser');
const geo = require('./routes/geo.route'); // Imports routes for the features

// initialize the express app
const app = express();

app.use('/geo', geo);

let port = 1234;

app.listen(port, () => {
    console.log('The server is running on port number ' + port);
});

Your folder structure should look like the following:

folder-structure

Run the following command in the terminal:

node app.js

Navigate to http://localhost:1234/geo/test in your browser, you should see the following:

hello-test

 

Postman

This is a good time to introduce Postman. Postman is a great tool to test our endpoints. Install Postman, and let's test the /test route. Set the drop down to GET and type the following url:

localhost:1234/geo/test

Click on the Send button, you should see the following:

postman-test

 

A MongoDB database hosted by mLab

Now that Postman is up and running we need to setup a database to store the GeoJSON data.

Head over to mLab and create an account, once you have done this, click on the Create New button to create a new MongoDB Deployment then select the FREE SANDBOX plan and click CONTINUE.

mlab-sandbox

Select your region and give your database a name – I have named mine geotutorial. Click CONTINUE and you should get a summary page similar to the following:

mlab-summary-order

If you are happy with the summary click SUBMIT ORDER and your new geotutorial deployment will be added to your deployment list.

Click on your new geotutorial deployment, notice the Mongo Shell connection string and URI options at the top of the page. Select the Users tab and click on the Add database user button.

mlab-newuser

Create a new user and make sure you leave the Make read-only check box un-checked, we will need read and write access to our database to enable our Create, Update and Delete endpoints.

mlab-user-logon

Now that we have a database and a user we can connect our app to our remote database.

 

Connecting the app to the database

Return to the app.js file and use the mongoose package that was installed at the start to add database connection parameters.

Edit the app.js file and make sure the dev_db_url variable is updated with the connection string of the remote database on mLab.

mongodb://<dbuser>:<dbpassword>@ds149606.mlab.com:49606/geotutorial

replace dbuser and dbpassword with the user and password you have assigned to your database. Numeric values in the url are ikely to be different so ensure you use the url for your database provided in mLab.

Body Parser

The final configuration needed in the app.js file is declaring the use of bodyParser.

In the app.js file, add the following:

app.use(bodyParser.json());
app.use(bodyParser.urlencoded({extended: false}));

The app.js file should now look like this:

// app.js
const express = require('express');
const bodyParser = require('body-parser');
const geo = require('./routes/geo.route'); // Imports routes for the features

// initialize the express app
const app = express();

// Set up mongoose connection
const mongoose = require('mongoose');
let dev_db_url = '<dbuser>:<dbpassword>@ds149606.mlab.com:49606/geotutorial';
const mongoDB = process.env.MONGODB_URI || dev_db_url;
mongoose.connect(mongoDB);
mongoose.Promise = global.Promise;
const db = mongoose.connection;
db.on('error', console.error.bind(console, 'MongoDB connection error:'));

app.use(bodyParser.json());
app.use(bodyParser.urlencoded({extended: false}));
app.use('/geo', geo);

let port = 1234;

app.listen(port, () => {
    console.log('The server is running on port number ' + port);
});

Everything should now be setup in the app.js file and we are ready to implement the CRUD end points. The only two files that require editing from this point on are routes/geo.route.js and controllers/geo.js.

 

The CRUD end points

 

CREATE

The first endpoint in our CRUD app is to Create a new GeoJSON feature, in this case a vector line. Let’s start by defining the route. In the geo.route.js file a create url path and an associated controller in the geo.controller.js file. The routes/geo.route.js file is where the url paths are configured so that when the app receives these paths from the browser, it can direct them to the associated controller logic in the controllers/geo.controller.js file that will communicate with the database. We will repeat this pattern for the Read, Update and Delete endpoints.

// routes/geo.route.js...
router.post('/create', geo_controller.geo_create);

Add the following function to the controllers/geo.controller.js file:

// controllers/geo.controller.js
/* CREATE */
exports.geo_create = function (req, res, next) {
    var geo = new Geo(
        req.body
    );

    geo.save(function (err) {
        if (err) {
            return next(err);
        }
        res.send('Feature created successfully')
    })
};

The function above creates a new GeoJSON feature using the data coming from a POST request and saves it to the database.

In Postman, send a POST request to the following url localhost:1234/geo/create and specify the POST data as:

{
    "type": "Feature",
    "name": "CREATETEST",
    "style": {
        "color": "#ff46b5",
        "weight": 10,
        "opacity": 0.85
    },
    "geometry": {
        "type": "LineString",
        "coordinates": [
            [
                -31.44379,
                -70.65775
            ],
            [
                -31.509339,
                -70.655823
            ],
            [
                -31.485817,
                -71.191406
            ],
            [
                -31.353473,
                -71.683044
            ],
            [
                -31.04576,
                -71.61748
            ]
        ]
    },
    "properties": {
        "direction": "Bidirectional",
        "power": -12
    }
}

Select the raw radio button in the Postman Body tab.

postman-create

Take a look at your mLab database you will see a new collection has been created with the name geos.

mlab-create

 

READ

The second endpoint in our CRUD app is to Read an existing feature. Add the following path to the routes/geo.route.js file:

// routes/geo.route.js...
router.get('/read_allgeo', geo_controller.geo_list);
// localhost:1234/geo/read_allgeo

Add a geo_list function to the controllers/geo.controller.js file:

// controllers/geo.controller.js
/* READ */
// return list of all geometry
exports.geo_list = function (req, res) {
    Geo.find({},{'geometry': 1}, function (err, geo) {
        if (err) return next(err);
        res.send(geo);
    })
};

In Postman, call the following url:

localhost:1234/geo/<featureid>

Where featureid is the id of the GeoJSON object that was created previously in the Create endpoint.

postman-read

The Read endpoint above returns all the GeoJSON features in our collection, for our front-end in Part 2 it will be useful to Read some other content. I have included 4 additional Read endpoints, these return specific features by calling the name or the id attributes and can return either only the geometry or name attributes. The idea is to build Read endpoints that return only what we need.

With the additional 4 READ end points added, the geo.route.js file looks like this:

// routes/geo.route.js...
router.get('/read_allgeo', geo_controller.geo_list);
// localhost:1234/geo/read_allgeo

router.get('/read_name/:name', geo_controller.geo_name);
// localhost:1234/geo/read_name/CREATETEST

router.get('/read_id/:id', geo_controller.geo_id);
// localhost:1234/geo/read_id/<id>

router.get('/read_allnames', geo_controller.geo_allnames);
// localhost:1234/geo/read_allnames

router.get('/read_all', geo_controller.geo_all);
// localhost:1234/geo/read_all

Notice the :id and :name content, these enable specific ids and names to be appended to the url and can return single features. With the additional 4 Read endpoints added, the geo.controller.js file looks like this:

// controllers/geo.controller.js
/* READ */
//return list of all geometry
exports.geo_list = function (req, res) {
    Geo.find({},{'geometry': 1}, function (err, geo) {
        if (err) return next(err);
        res.send(geo);
    })
};

//return item that matches name
exports.geo_name = function (req, res) {
    Geo.findOne({ name: req.params.name },{}, function (err, geo) {
        if (err) return next(err);
        res.send(geo);
    })
};

//return item that matches id
exports.geo_id = function (req, res) {
    Geo.findById(req.params.id, function (err, geo) {
        if (err) return next(err);
        res.send(geo);
    })
};

//return all names only
exports.geo_allnames = function (req, res) {
    Geo.find({},{'name': 1}, function (err, geo) {
        if (err) return next(err);
        res.json(geo);
    });
};

//return geometry only
exports.geo_all = function(req,res) {
    Geo.find({},{}, function(err, geo){
        if (err) return next(err);
        res.send(geo)
    });
};

 

UPDATE

The third endpoint in our CRUD app updates an existing GeoJSON object. Add the following path to the routes/geo.route.js file:

// routes/geo.route.js...
/* UPDATE */
router.put('/update/:id', crud_controller.feature_update);
//localhost:1234/features/update/<featureid>
//BODY {"geometry":{"coordinates":[[-34.44379,-70.65775],[-34.199626,-71.106262],[-34.04576,-71.61748]]}}

Add the feature_update controller to the controllers/geo.controller.js file:

// controllers/geo.controller.js
/* UPDATE */
exports.feature_update = function (req, res, next) {
    Geo.findByIdAndUpdate(req.params.id, {$set: req.body}, function (err, geo) {
        if (err) return next(err);
        res.send('Geometry updated.');
    });
};

postman-update

Take a look at your collection and notice the geometry has been updated.

 

DELETE

The last endpoint in our CRUD app deletes an existing GeoJSON object. Add the following path to the routes/geo.route.js file:

// routes/geo.route.js...
/* DELETE */
router.delete('/delete/:id', geo_controller.geo_delete);
//localhost:1234/geo/delete/<featureid>

Add the geo_delete controller to the controllers/geo.controller.js file:

// controllers/geo.controller.js
/* DELETE */
exports.geo_delete = function (req, res) {
    Geo.findByIdAndRemove(req.params.id, function (err) {
        if (err) return next(err);
        res.send('Deleted successfully!');
    })
};

The function simply deletes an existing GeoJSON object.

In Postman, call the following url:

localhost:1234/geo/<featureid>/delete

Where featureid is the id of the GeoJSON object we created using the Create endpoint.

postman-delete

 

Thats the end of the tutorial, the full code can be found on GitHub. Be sure to check out Part 2 of the workshop which I am planning to make available in the next few weeks. In Part 2 we will extend the Leaflet.js mapping library using the Leaflet.Editable.js library as a front-end editing tool to communicate with the RESTful API Create, Read, Update and Delete functionality built in this tutorial.