June 8, 2018

Photo Gallery Part 2 - Backend

Part 2 of building our Photo Gallery using Node, Express, Angular, and Mongo

Photo Gallery Part 2 - Backend

Welcome back everyone. It's now time to continue working on our photo gallery application. This time, we will be setting up all of the back-end server code. If you missed it, be sure to check out Part 1 where we setup the project and installed the Angular front-end.

For the back-end, we are going to be using NODE and the Express framework. We will also install a few other dependencies, such as mongoose for interacting with the DB and multer for dealing with multi part form submission (e.g. file uploads)

Back-end Setup

The first thing we are going to do is initialize our NPM project and install the needed back-end dependencies.

Start by opening a terminal window and navigating to your project folder. Be sure to be in the root of your project and not within the angular-src folder.

We are going to start by creating a blank NPM project

npm init

This will provide you a list of questions. Feel free to use the defaults for all of them. The only one that I changed was the entry point and set it to 'server.js' instead of 'index.js'

Next we are going to install a few dependencies

npm install --save express body-parser multer mongoose

ExpressJS - This package allows us to easily create a web server inside node as it does a lot of the heavy lifting for us.

body-parser - Body Parser use to be part of Express but has now been broken down into its own project. Body Parser is a Node middleware which looks at all of the requests our server gets and puts them into JSON format for easier manipulation in our code.

multer - Very similar to body-parser, multer is another middleware library which deals with multi-part form submission. In our case, we are going to use it to deal with users uploading pictures to the server.

mongoose - Last, we are installing a MongoDB library known as Mongoose. There are a few to choose from, but I prefer working with Mongoose.

Options Installs

You don't need this next part, but I normally install nodemon for all of my NODE projects. Since you will be changing the server side code often, nodemon will monitor your project for changes and automatically restart the server. This saves a lot of time and sometimes confusion because without it you would have to manually stop and start the server after every change.

npm install --save-dev nodemon

We are installing it as a development dependencies because it is only needed in development and not needed for the production build of our project. You may also see people suggest to install nodemon as a global package (npm install -g nodemon). Both approaches will work. I prefer installing it local to the project because I know it's always going to be there, and it helps with version control when you have multiple projects on your system.

Folder Structure

Before we jump into the code, lets structure out the folder structure and create some of the file we will need.

The first thing that you will notice is that our project folder now has package.json, package-lock.json, and node_modules in it. These were created for us when we created our project and installed the extra libraries.

Still inside the root folder of your project, create a server.js file, a models folder and a routes folder. You can do this inside the terminal, file manager, or in your text editor - whichever you'd like

From there, our project should now have the following folders and files in it

angular-src/
models/
node_modules/
public/
routes/
package-lock.json
package.json
server.js

Don't worry about understanding what everything is going to be used for just yet. We will dive into the details now that the basics are there.

Code

Basic Web Server

We are going to start by creating a very basic express web server, which we will come back after to update as our project grows.

Inside server.js, we start by adding our dependencies and initializing our application.

// Import Dependencies
const express = require('express');
const bodyParser = require('body-parser');
const mongoose = require('mongoose');
const path = require('path');
const multer = require('multer')

// Init app and set frontend static files
// We are cheating here by creating a global variable which we will use later. This is not recommonded for production code.
const app = express()
app.use(express.static(path.join(__dirname, 'public')));
global.__basedir = __dirname;

The next part is to add our middleware. For this, we are telling body parser to take all of the JSON data it gets from the front-end and put it in a new format for us.

// MIDDLEWARE //
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json({ type: 'application/json' }));

Below that, we are going to add 1 route to our server. A route is a location the user is allowed to go to. For example, if our webapp was example.com/myapp, example.com would be the server and /myapp would be the route. For now, we are just going to create one basic route to make sure that everything is working.

// ROUTES //
app.use('/api/test', (req, res) => {
    res.send('Hello, friend!');
});

This will print Hello, Friend when we go to /api/test.

Finally, we have to start our server with the below.

app.listen(3000, () => {
    console.log('Server started on port 3000...');
};

Before we move forward, lets test to make sure that everything is working properly. Inside your terminal, execute this command in the project folder

nodemon server.js

After you see the message 'Server started on port 3000' open a web browser and go to 'localhost:3000/api/test' and you should see 'Hello, Friend!' on the screen. If you received any errors or don't see the message, go back and recheck your code.

Adding Routes

To make our code a little cleaner and easier to read, we are going to put most of our routes code inside their own file. This not only reduces the amount of code in server.js but it also allows you to know what file each part of your code is in.

First, add this below the test route we created earlier

// ROUTES //
app.use('/api/test', (req, res) => {
    res.send('Hello, friend!');
});

app.use('/api/pictures', require('./routes/pictures'));
app.use('/api/albums', require('./routes/albums'));
app.use('/public/uploads/:album/:photo', (req, res) => {
    res.sendFile(path.join(__dirname, 'public/uploads', req.params.album, req.params.photo));
});
app.get('*', (req, res) => {
    res.sendFile(path.join(__dirname, 'public/index.html'));
});

We have now added some logic that tells node that all of the code for /api/pictures and /api/albums are in their own files.
After that, we are adding a specific route, which we will use to send the saved pictures back to the web browser. Lastly, we say that any other route should redirect to our index.html page. This is a one of many ways to get Node and Angular to work together.

Inside the routes folder, create a albums.js and add

const express = require('express');
const router = express.Router();
const path = require('path');
const fs = require('fs');
const multer = require('multer');

const Album = require('../models/album');
const DIR = 'public/uploads';
const upload = multer({ desc: DIR });

// TEST //
router.get('/test', (req, res, next) => {
    res.send('Hello, Friend!');
});

// GET //
router.get('/', (req, res, next) => {
    Album.getAll((err, data) => {
        if (err) {
            return res.json({ error: err });
        }
        res.json({ albums: data });
    });
});

router.get('/:id', (req, res, next) => {
    Album.get(req.params.id, (err, data) => {
        if (err) {
            return res.json({ success: false, error: err });
        }

        dir = path.join(__basedir, 'public', 'uploads', data._id.toString());
        fs.readdir(dir, (err, items) => {
            if (err) {
                console.log('ERROR: ', err);
            }
            res.json({ success: true, album: data, pictures: items });
        });


    });
});

// POST //
router.post('/', (req, res, next) => {
    const newAlbum = new Album({
        name: req.body.name,
        description: req.body.description
    });

    Album.add(newAlbum, (err, data) => {
        if (err) {
            return res.json({ error: err });
        }

        res.json({ album: data });
    });
});

// PUT //
router.put('/:id', (req, res, next) => {
    Album.get(req.params.id, (err, album) => {
        if (err || !album) {
            return res.json({ success: false, error: err });
        }

        if (req.body.name) {
            album.name = req.body.name;
        }
        if (req.body.description) {
            album.description = req.body.description;
        }

        Album.edit(album, (err, data) => {
            if (err) {
                return res.json({ success: false, error: err });
            }
            res.json({ success: true, album: data });
        });
    });
});

// DELETE //
router.delete('/:id', (req, res, next) => {
    Album.remove(req.params.id, (err, data) => {
        if (err) {
            return res.json({ success: false, error: err });
        }
        res.json({ success: true, data: data });
    });
});

module.exports = router;

This file deals with creating, reading, updating, and deleting albums from our app. This process is also known as CRUD(Create, Read, Update, Delete).

We are going to do the same for our pictures route. First by creating pictures.js inside the routes directory and then add this code

const express = require('express');
const router = express.Router();
const path = require('path');
const fs = require('fs');
const multer = require('multer');

var filename = '';
const Album = require('../models/album');
// Multer Config //
const DIR = 'public/uploads';
// Needed when storing on disk
const storage = multer.diskStorage({
    destination: function(req, file, cb) {
        cb(null, 'public/uploads');
    },
    filename: (req, file, cb) => {
        const parts = file.originalname.split('.');
        filename = parts[0] + '-' + Date.now() + '.' + parts[1]
        cb(null, filename);
    }
});
const upload = multer({ storage: storage, preservePath: true });


// TEST //
router.get('/test', (req, res, next) => {
    res.send('Hello, Friend!');
});

// GET //
router.get('/', (req, res, next) => {

});

router.get('/:id', (req, res, next) => {

});

// POST //
router.post('/', upload.single('picture'), (req, res, next) => {
    // Save image to disk
    if (!fs.exists('public/uploads/' + req.body.album)) {
        console.log('here');
        fs.mkdir('public/uploads/' + req.body.album, (err, data) => {
            fs.rename('public/uploads/' + filename, 'public/uploads/' + req.body.album + '/' + filename);
        });
    } else {
        fs.copyFile('public/uploads/' + filename, 'public/uploads/' + req.body.album, (err, data) => {
            console.log(data);
        });
    }
    return res.json({ success: true });

    // console.log('pic: ', req.file);
    // console.log('body:', req.body);
});

// PUT //
router.put('/:id', (req, res, next) => {

});

// DELETE //
router.delete('/:id', (req, res, next) => {

});

module.exports = router;

At this point you are properly getting a few errors which is preventing your code from running. This is expected, as we have not finished adding everything. Continue on with the next few parts to get your code into a working state.

So far we have created a basic web server and have routes for dealing with album and picture requests. All of this is great, but we need a place to save all of that information. For now, we are going to save the pictures to the disk and create a mongo database to keep track of the album names. In future posts we are going to learn how to save the pictures right inside the database.

To prepare for the future, we are going to setup the database code now. We do that by creating 'album.js' inside the modals folder and adding the following code.

const mongoose = require('mongoose');

mongoose.Promise = require('bluebird');

const AlbumSchema = mongoose.Schema({
    name: { type: String, required: true },
    description: { type: String },
    pictures: [{
        data: Buffer,
        contentType: String,
    }]
});

const Album = module.exports = mongoose.model('album', AlbumSchema);

module.exports.add = (newAlbum, callback) => {
    newAlbum.save(callback);
};

module.exports.edit = (album, callback) => {
    album.save(callback);
};

module.exports.remove = (id, callback) => {
    Album.findByIdAndRemove(id, callback);
};

module.exports.get = (id, callback) => {
    Album.findById(id, callback);
};

module.exports.getAll = (callback) => {
    Album.find()
        .select('-pictures')
        .exec(callback);
};

This follows the same CRUD flow as our route code, but we are dealing with the database operations and in the routes we are dealing with the user requests.

Before we can use the mongo database, we have to tell our code to connect to mongo. We do this be adding this inside our app.listen code block in 'server.js'

// START SERVER //
app.listen(3000, () => {
    console.log('Server started on port 3000...');

    // DB //
    mongoose.connect('mongodb://localhost:27017/ng5-photogallery');

    mongoose.connection.on('connected', () => {
        console.log('Connected to database... ');
    });
    mongoose.connection.on('error', (err) => {
        console.log('Database Error: ', err);
    });
});

After saving all of your files, nodemon should relaunch your server without any errors.

Conclusion

We now have a working web server with all of the code we will need for the front-end. Although all of this code could have gone inside 'server.js', it would have made the file very large and hard to read and maintain. Instead, we learned how to create a very module code base which is easier for us and others to understand.

Normally the next step would be to create unit tests and test our back-end. We are going to skip that part for now and move right into create the front-end in our next session.

As always, you can find the complete code in my GitHub repository.

GitHub Repo

Happy Coding!