Based on a number of comments, and the level of interest, I have updated this tutorial which I originally posted in 2014 addressing items, including, amongst other things, removing Mapbox libraries and updating the template engine from jade to pug. The most recent update took place in September 2018 and includes JQuery and Leaflet library updates and a debug dependency upgrade to address security vulnerability. I tested the tutorial in both Firefox and Chrome browsers. Please connect if you have any feedback.

I wanted to explore the Node.js environment with a view to building a very simple mapping application. Initial searches didn't reveal too much in the way of Leaflet, Node.js and MongoDB examples other than the OpenShift plug example by Steve Pousty back in 2014, which is a great start but I wanted a few more layers and an excuse to use the pug template engine (formerly jade). I attended an informative workshop by Steve Pousty at the 2014 FOSS4G event in Portland OR, same bundle, but with Flask rather than Node.js. I would highly recommend anyone starting out to spend some time on researching the fundamentals - here are a few links I found useful.

I decided to put a simple tutorial together in the hope that others might benefit from my efforts. Initially I found Node.js to be rather intimidating, it's really not, but there are many third party Node.js programs, these vary from example to example and can be overwhelming. My suggestion is to start off simple, figure out what you need, and as you learn more, start exploring.The Node.js community is a great resource.

I am all about embracing best practice so if anyone has suggestions on how to improve on my content, please reach out, I would love to hear from you.

Here is what I want to achieve from my Node.js application:

  • Build something similar to the Mapbox Toggling layers example
  • Add point, line and polygon layers to a Leaflet map
  • Store layers as GeoJSON in MongoDB
  • Use the pug (formerly jade) template engine to build HTML based on layers
In the example I will only have three layers but the design is intended to be scalable and should be able to accommodate more content without the need to build custom HTML.

 

Installation

If you use Windows or OS X you can get the latest version from the Node.js website it should be a simple install and comes with the Node Package Manager, NPM which is really useful for installing the packages for your application. You will be using NPM a lot but don't worry it's fairly straightforward.

For Debian based distributions like Ubuntu, Node.js is available using the package manager. DigitalOcean is a great resource.

Open your terminal/command line and make a directory in which you will place your applications, I have made a directory called projects/nodejs. cd into the directory you have just made.

Next we are going to install the Express web framework - I found this framework really useful as it generates a collection of template files and folders that make sense and provide structure to our project.

sudo npm install express-generator -g

This command makes the Express functionality available globally so we can access it from any test application projects we build in the future. The global installation of Node.js Packages have to be run as root, so it must be run with admin priviledges.

 

Creating a project

We are now ready to crate a project. If you are not already there, cd into the directory you made and think of a name for your new project, I am going with leaflet_map. MDM web docs is a great resource. To create your leaflet_map project, execute the following command, where leaflet_map is whatever name you have chosen for your project.

express -e --pug leaflet_map

The -e --pug tells express that we want to use the pug template engine.
This is kind of a magical moment as Node.js and Express get to work generating a project template for you. You should see a series of lines that begin with 'create' returned in the console. Take a look at the contents of your parent directory, you will notice there is now a directory with the name of your project. Take a look inside your project folder, you should see the following:

  • app.js - application nerve center
  • bin directory - internal workings
  • package.json - handles dependencies
  • public directory - location for images, javascripts and stylesheets
  • routes directory - kind of like a switchboard, handles and directs requests
  • views directory - where the HTML output is constructed
If you take a look in your views directory you will see a few files with a .pug extension, these are the pug template files and will be used to generate HTML. To learn more about the pug template engine visit [https://github.com/pugjs/pug](https://github.com/pugjs/pug) Pug is a little weird at first but if you know some HTML you should be up and running in no time - the key to pug is indentation. If you would prefer not to use pug, you have other options, there are plenty of template engines to choose from including. You may have heard of haml, handlebars or mustache, now is a good time to configure your index.js if you would like to use one of these, you could also use the express ejs option which is more in-line with raw HTML.

 

Add Dependencies

Open the package.json file in your editor of choice, you will notice that it is a basic JSON file with a short description of the app and a list of the dependencies. Since we will be working with MongoDB we need to add two dependencies. In your package.json file type in the mongodb and mongoose dependencies seen below - don't forget the trailing comma on all dependency lines apart from the last.

When working with JSON or GeoJSON there are some useful syntax checkers like http://jsonlint.com and http://geojsonlint.com, if you have any doubt about your json syntax, these are useful resources. Another handy JSON resource is the command line tool https://www.npmjs.org/package/jsonlint - it'll point out where your syntax errors exist - then you will know whether it's syntax related or something else. The asterisks in the lines we have added instruct NPM to get the latest version of that dependency. Mongoose is a driver that Node.js uses to talk to MongoDB.

// package.json
{
  "name": "leaflet_map",
  "version": "0.0.0",
  "private": true,
  "scripts": {
    "start": "node ./bin/www"
  },
  "dependencies": {
    "cookie-parser": "~1.4.3",
    "debug": "~2.6.9",
    "express": "~4.16.0",
    "http-errors": "~1.6.2",
    "morgan": "~1.9.0",
    "pug": "2.0.0-beta11",
    "mongodb": "*",
    "mongoose": "*"
  }
}

Save the edits you made to the package.json file. To install the dependencies we have added we need to tell NPM to review our package.json file and take the necessary steps to get our new dependencies plugged in. To do this, execute the following command from inside your project folder.

npm install

Now run the following:

npm start

The cogs will start turning and in a few seconds your new dependencies should be officially hooked up. At this stage you can run your application in the browser - http://localhost:3000. If you are using a server, adjust local host to reflect your remote IP.

You should see the words 'Express' and 'Welcome to Express' in your browser and the following in your console:

> leaflet-map@0.0.0 start /home/username/projects/nodejs/leaflet_map
> node ./bin/www

To exit out of your application, type Ctrl-C.

If you are interested in seeing where the text displayed in your browser came from, take a look at the routes/index.js file, you should see the following:

// routes/index.js
var express = require('express');
var router = express.Router();

/* GET home page. */
router.get('/', function(req, res) {
  res.render('index', { title: 'Express' });
});

module.exports = router;

At the start of the tutorial I compared the functionality handled by content in the routing directory to a switchboard. The index.js file receives a request from the URL http://localhost:3000 it then calls the res.render function which takes two arguments, firstly, 'index' which instructs the application to use the index.pug template file in the views directory and secondly, a JSON object with a name of title and a value of 'Express'. This is the text that displays in the browser, but lets see how that's done by looking at the views/index.pug file

// views/index.pug
extends layout

block content
  h1= title
  p Welcome to #{title}

This is what the content of a pug template file looks like, notice the indentation. The first line imports anything in the layout.pug file, this is a nice feature because it means we can get a template structure setup in the layout.pug file and use this for consistency across our site. The block content line is telling the pug template engine where the content from the layout file needs to go. Next the title variable passed from routes/index.pug, which in this case is 'Express' is assigned to an HTML <h1> tag then under that, a paragraph <p> with the text 'Welcome to' and again our title parameter is passed in, so it will read 'Welcome to Express' - and that is the content displayed in your browser. If you are feeling confident, make some changes to the routes/index.js and views/index.pug files to reflect your own content. You really don't have much to lose, so I would encourage making some edits - if you get totally lost, simply delete the folder and run express -e --pug <new_project> and start over.

Up till now we have pretty much covered what you are likely to find in most Node.js tutorials, the next steps will move us in the direction of our mapping application.

 

MongoDB

In your browser, head over to https://www.mongodb.org/ to download the latest version of MongoDB. This link is a very informative MongoDB install resource - follow the install setup in this link. To start the MongoDB service execute the following command:

sudo service mongod start

To stop the MongoDB service execute the following command:

sudo service mongod stop

While the service is running, open another terminal/command line and execute the following command. Starting and stopping mongodb as a service requires root-access. If you used service you shouldn't need another terminal window since it does it for you in the background and you can still use your terminal.

mongo --host 127.0.0.1:27017

Something similar to the following should be returned in the console:

MongoDB shell version: 3.6.4
connecting to: mongodb://127.0.0.1:27017

This is the default, we will be changing that to a new database. I will not be going into detail on how to use MongoDB, please refer to the MongoDB documentation to learn more. Lets go ahead and create a database with the same name as our project - at the mongo prompt type the following.

use leaflet_map

This will get us an empty database by the name of leaflet_map and return the following:

switched to db leaflet_map

Lets add some GeoJSON data to MongoDB, we can also get some sample data from the MongoDB website. Notice I have added a name to each layer and placed double quotes around 'name', 'type' and 'coordinates' field names. We will add the GeoJSON data to a collection called layercollection. Run the following commands at the mongo prompt.

db.layercollection.insert({ 
    "type": "MultiPoint",
    "name": "points",
    "color": "#0000ff",
    "style": {
        "radius": 8,
        "fillColor": "#00ce00",
        "color": "#008c00",
        "weight": 2,
        "opacity": 1,
        "fillOpacity": 1
    },
    "coordinates": [
       [-73.9580, 40.8003 ],
       [-73.9498, 40.7968  ],
       [ -73.9737, 40.7648 ],
       [ -73.9814, 40.7681 ]
    ]
})
db.layercollection.insert({
  "type": "MultiLineString",
  "name": "lines",
  "style": {
    "color": "#ff46b5",
    "weight": 10,
    "opacity": 0.85
 },
  "coordinates": [
     [ [ -73.96943, 40.78519 ], [ -73.96082, 40.78095 ] ],
     [ [ -73.96415, 40.79229 ], [ -73.95544, 40.78854 ] ],
     [ [ -73.97162, 40.78205 ], [ -73.96374, 40.77715 ] ],
     [ [ -73.97880, 40.77247 ], [ -73.97036, 40.76811 ] ]
  ]
})
db.layercollection.insert({ 
    "type": "FeatureCollection", 
     "name": "polygons", 
    "features":  
    [{ 
    "type": "Feature", 
    "properties": {"style": "Orange", "name": "Orange"}, 
    "geometry": { 
        "type": "Polygon", 
        "coordinates":  [ [  
            [  -73.9814, 40.7681 ], [ -73.958, 40.8003 ], [ -73.9737, 40.7648 ], [ -73.9814, 40.7681 ] 
        ] ] 
    } 
}, { 
    "type": "Feature", 
    "properties": {"style": "Blue", "name": "Blue"}, 
    "geometry": { 
        "type": "Polygon", 
        "coordinates":  [ [  
            [ -73.958, 40.8003 ], [ -73.9498, 40.7968 ], [ -73.9737, 40.7648 ], [ -73.958, 40.8003 ] 
        ] ] 
    } 
}] 
})

After each of the above inserts, you should see the following response:

WriteResult({ "nInserted" : 1 })

To view the collection, at the prompt run:

db.layercollection.find().pretty()

Now that we have spatial data in MongoDB lets get it wired up for us to use in our application.

 

The Node.js MongoDB connection

To setup our MongoDB connection, edit the routes/index.js file to include the following.

// routes/index.js
var express = require('express');
var router = express.Router();

// Mongoose import
var mongoose = require('mongoose');

// Mongoose connection to MongoDB
mongoose.connect('mongodb://localhost/leaflet_map', { useNewUrlParser: true }, function (error) {
    if (error) {
        console.log(error);
    }
});

First we set the mongoose variable then below that, a connection to MongoDB 'mongodb://localhost/leaflet_map'.

Continuing with our edits to the routes/index.js file, we define the Mongoose schema which we will call JsonSchema and below that the Mongoose model which we will call Json. Notice in the Json model definition there are three arguments - I didn't find mongoose documentation too helpful if you already had data in the database the first argument is a name the second is the schema and the third is the collection, in our case leaflet_map.

// routes/index.js
// Mongoose Schema definition
var Schema = mongoose.Schema;
var JsonSchema = new Schema({
    name: String,
    type: Schema.Types.Mixed
});
 
// Mongoose Model definition
var Json = mongoose.model('JString', JsonSchema, 'layercollection');

We now need to set up an app item that handles a request for MongoDB GeoJSON data based on the name of our layer, we are going to use http://localhost:3000/mapjson/<layer_name> to do this. You should be somewhat familiar with the GET home page handler we looked at earlier, and we can leave that here for reference.

We tell the handler that we want a URL called http://localhost:3000/mapjson that takes a layer name as an argument. We then confirm that the layer parameter exists and use the MongoDB findOne function to return one layer item where the name in the MongoDB database collection matches the name of the parameter. The findOne function is appended to the Json model we defined above.

// routes/index.js
/* GET home page. */
router.get('/', function(req, res) {
  res.render('index', { title: 'Express' });
});

/* GET json data. */
router.get('/mapjson/:name', function (req, res) {
    if (req.params.name) {
        Json.findOne({ name: req.params.name },{}, function (err, docs) {
            res.json(docs);
        });
    }
});

Next we build a handler to get us all the layer names and return them as JSON. This handler is very similar to the handler above except we do not want to restrict the result to only one record so we use find rather than findOne. We are also not accepting parameters on which to base our result and lastly we only want the name field returned in the JSON array. We will use this handler to iterate through our layers and add them to the map with the help of our GeoJSON handler above.

// routes/index.js
/* GET layers json data. */
router.get('/maplayers', function (req, res) {
    Json.find({},{'name': 1}, function (err, docs) {
        res.json(docs);
    });
});

Finally, we are going to setup a page to display based on a request for the http://localhost:3000/map/ URL. We are going to send a jmap JSON object and lat lng variables to the map.pug template. These will be used to iterate through and generate HTML based on our layers and center our map at the lat and lng coordinates provided. The lat lng coordinates could just as easily be stored in our MongoDB collection.

// routes/index.js
/* GET Map page. */
router.get('/map', function(req,res) {
    Json.find({},{}, function(e,docs){
        res.render('map', {
            "jmap" : docs,
            lat : 40.78854,
            lng : -73.96374
        });
    });
});

module.exports = router;

If you have been paying attention you will notice we are sending the JSON, lat and lng variables to a map.pug template that does not yet exist - lets go and create this file. But before we create the map.pug file lets take a look at our MongoDB data in the browser. Execute the following command.

npm start

Go to http://localhost:3000/mapjson - did you get a 404 error? well that's because http://localhost:3000/mapjson takes a layer name argument - try again with the following: http://localhost:3000/mapjson/points and you should get your points GeoJSON returned. Hopefully this is starting to make sense. Lets create our map.pug file so we can display something other than JSON.

 

Show me the map

If you are still with me at this point - well done. Lets move on and create a views/map.pug file. If you like you can copy the index.pug file and rename it. At this point we will get our hands dirty with some pug templating and build our map content.

The views/map.pug file is going to have a similar structure to the the Mapbox Toggling layers example with the exception of the pug Template iteration and calls to the handler URLs to return our JSON and GeoJSON data and, we will only be using the Leaflet library. We could put everything in one file like the example but let's rather get our style sheet and JavaScript references setup in the layout.pug file.

//views/layout.pug
doctype html
html
    head
        title= title
        link(rel='stylesheet', href='https://cdn.leafletjs.com/leaflet/v0.7.7/leaflet.css')
        link(rel='stylesheet', href='https://ajax.googleapis.com/ajax/libs/jqueryui/1.11.4/themes/smoothness/jquery-ui.css')
        link(rel='stylesheet', href='/stylesheets/style.css')
        script(src='https://ajax.googleapis.com/ajax/libs/jquery/1.12.2/jquery.min.js')
        script(src='https://ajax.googleapis.com/ajax/libs/jqueryui/1.11.4/jquery-ui.min.js')
        script(src='https://cdn.leafletjs.com/leaflet/v0.7.7/leaflet.js')
    body
        block content

Take a look at the pug template engine to learn more about how to setup your content and the format you need to use; notice the indentation. In the views/layout.pug file you will see I have setup the JavaScript and style sheet references for your application. There is not much point in doing this for our example as we only have a one page application but you might use the layout.pug file to setup your bootstrap page template - assuming other applications you build may have multiple pages - this will get you a consistent layout throughout your site.

Let's edit our views/map.pug file, I will break down the content into three parts, the first is the HTML content.

// views/map.pug part 1
extends layout.pug
block content
    #map
    #leg_title 
        span#leg_items Map Legend
    #leg
        each layer, i in jmap
            input(id=layer.name)(type='checkbox', checked)
            span#leg_items #{layer.name}
            br

Again, notice the indentation, this is important when working with pug. If your indentation is not correct, depending on the location of the indentation you might receive the following error: unexpected token "indent", or simply return a blank screen. The first two lines are where the file will inherit the content from the layout.pug file. Next we add an HTML map div element - so the pug template engine translates #map to <div id='map'></div>. We then add HTML div elements for legend title and legend body. Notice the line beginning 'each...' this is where the template magic kicks in - if you recall in the routes/index.js file, we are passing in a JSON object called jmap - the pug template now has access to this jmap variable, so that enables us to loop through the layer names available to us based on the content of our MongoDB database and output a checkbox and label for each item.

I have added some style content to the /public/stylesheets/style.css file needed to position the map and legend content correctly so be sure to get that setup before running your final product. The full source code is available on GitHub.

In the second part of our views/map.pug file we setup the Leaflet content and you will notice in the setView function we have added lat and lng arguments, these are being passed in from the routes/index.js file. The final four lines are where the handler magic happens, we make a getJSON request from /maplayers for the JSON that contains our layer names. Once we have our layer names we use the jQuery each function to iterate through our names and add arguments to the addLayer function call. The first argument sets up a Mapbox feature layer - using the loadURL function it makes a request from the mapjson/ handler we setup and uses the layer name from the each iteration as an argument. The final argument is the layer name itself.

//views/map.pug part 2
script(type='text/javascript').
        var map = L.map('map').setView([#{lat},#{lng}], 14);
        L.tileLayer('http://{s}.tile.osm.org/{z}/{x}/{y}.png', {
            attribution: '&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
        }).addTo(map);
        
        $.getJSON('/maplayers',function(result){
            $.each(result, function(i, mlayer){
                $.getJSON('/mapjson/' + mlayer.name, function(data) { addLayer(data, mlayer.name ) });
            });
        });

The final part of the map.pug file is the addLayer function. The first part of this function adds our point, line and polygon layers to the map and binds a popup and style to each layer. Right under that is where we append a click function to the layer check boxes we created in part 1 above - so each checkbox represents a layer that the user can toggle to turn that particular layer on or off.

// views/map.pug part 3
function addLayer(layer, name) {
            var leaf_layer;
            if (layer.type == "MultiPoint") {
                leaf_layer = L.geoJson(layer, { pointToLayer: function (feature, latlng) {return L.circleMarker(latlng, layer.style); }})
                leaf_layer.bindPopup(layer.type);
            } else if (layer.type == "MultiLineString") {
                leaf_layer = L.geoJson(layer, {style: layer.style });
                leaf_layer.bindPopup(layer.type);
            } else  {
                leaf_layer = L.geoJson(layer, {
                    style: function(feature) {
                        switch (feature.properties.style) {
                        case 'Orange': return {color: "#ff0000"};
                        case 'Blue': return {color: "#0000ff"};
                    }
                    },
                    onEachFeature: function (feature, layer) {
                         layer.bindPopup(feature.properties.name);
                     }
                 });
            }
            leaf_layer.addTo(map);
            
            $('#' + name).click(function(e) {
                
                if (map.hasLayer(leaf_layer)) {
                    map.removeLayer(leaf_layer);
                } else {
                    map.addLayer(leaf_layer);
                }
            });
        }}

Navigate to your public folder then populate the style.css file with the following:

html, body {
  height: 100%;
  overflow: hidden;
}
#map {
  height: 100%;
}
body {
  margin:0;
  padding:0px;
  font:14px "Lucida Grande", Helvetica, Arial, sans-serif;
}
a {
  color:#00B7FF;
}
#leg, #leg_title {
  position:absolute;
  top:50px;
  right:10px;
  width:100px;
  padding:10px;
  background:rgba(34,34,34,1);
  color:#999;
  font-family: Arial, Helvetica, sans-serif;
  font-size:12px;
  line-height:18px;
  border-radius:3px;
  max-height:80%;
  overflow:auto;
}
#leg_title {
  top:10px;
}
#leg_items {
  position:relative;
  margin-left:5px;
  top:-1px
}

That's it, you are now ready to run your application, if you are not already there, cd to your project directory and run.

npm start

Head over to your browser and display your Leaflet Node.js application http://localhost:3000/map.

Summary

Depending on what you have in mind for your application build, you can construct request handlers in the routes/index.js file to handle data requests and page displays. If you recall from our example, we setup a /mapjson handler that takes a layer name as an argument to return GeoJSON for a particular layer. We also setup a /maplayers handler that returned the JSON for all our layer names, of course, we could adjust this to include other content such as alias names or style information. By combining URL handlers and the power of the pug template engine we have all the JSON we need at our disposal to use in JavaScript and the ability to generate HTML on the fly.

Node.js is fun to work with and might just be my new favorite framework, I am definitely going to spend more time getting to know it better and explore some of the many programs that are available to extend its functionality.

The full source code is available on GitHub.