Introduction

This project strives to provide a state-of-the art boilerplate for the MEAN stack (MongoDB-Express-Angular-Node) fully written in typescript. Another requirement of this project is 100% coverage with unit tests (also written in typescript!) for the boilerplate code. The whole boilerplate / seed is available on Github.


However, sometimes it helps to build something from scratch. For example if you only want the very basics, you could leave away the the authentication. Therefore this page contains step-by-step instructions (a tutorial, if you will) on how this boilerplate code is setup.

Motivation

Why am I motivated to build this boilerplate code? The MEAN stack was quite popular for a while, but it lost a bit of interest since the original projects aren't maintained very well. Yet, the stack might still be a good choice for some people. Here's what I see as its main advantages:

Having said this, I'd also like to warn you about this stack at this point. I think MongoDB is not the best choice to back your web-app with, here are my reasons why: https://www.bersling.com/2017/06/02/please-dont-use-mongodb-or-any-other-nosql-database-for-your-web-app/

Second, if your reason for choosing this stack is that you need node / javascript for its asynchronous nature to have a non-blocking server that can handle thousands of concurrent requests, I'd like to point out that this isn't exclusive to node / javascript. It's also possible to write non-blocking code in other languages and frameworks. It's just that people usually don't write their code async-style, thus you will find less packages & community discussions to help you out.

Basic Setup

To get started, first create the folder structure. Create your project folder:

mkdir project-name
cd project-name
mkdir backend
cd backend

Replace the red bits with your own values.

Then, first you'll need a package.json:

backend/package.json
{
  "name": "project-name",
  "version": "0.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "mocha --reporter spec --timeout 15000  --compilers ts:ts-node/register '**/*.test.ts'",
    "windows_test": "mocha --reporter spec --timeout 15000 --compilers ts:ts-node/register **\\*.test.ts",
    "start": "tsc && node dist/index.js",
    "spec": "mocha --reporter spec --compilers ts:ts-node/register --grep ${TEST} '**/*.test.ts'",
    "lint": "tslint src/**/*.ts",
    "forever": "tsc && forever stopall && forever start dist/index.js"
  },
  "author": "Your Name <youremail@gmail.com>",
  "license": "MIT",
  "dependencies": {
    "body-parser": "^1.15.2",
    "connect-flash": "^0.1.1",
    "cookie-parser": "^1.4.3",
    "winston": "^2.3.1",
    "winston-color": "^1.0.0"
  },
  "devDependencies": {
    "@types/body-parser": "^0.0.33",
    "@types/chai": "^3.4.34",
    "@types/chai-http": "^0.0.29",
    "@types/connect-flash": "^0.0.31",
    "@types/cookie-parser": "^1.3.30",
    "@types/mocha": "^2.2.32",
    "@types/node": "^6.0.46",
    "@types/winston": "^2.3.0",
    "chai": "^3.5.0",
    "chai-http": "^3.0.0",
    "mocha": "^3.1.2",
    "node-ssh": "^4.2.2",
    "ts-node": "^1.6.1",
    "typescript": "^2.0.6"
  }
}

As we progress, we'll install some more packages, but that's it for now.

Now you're ready to install those dependencies. Simply run

npm install

Since everything is in typescript, we're also going to need a tsconfig.json.

backend/tsconfig.json
{
  "compilerOptions": {
    "module": "commonjs",
    "target": "es6",
    "moduleResolution": "node",
    "sourceMap": true,
    "outDir": "dist",
    "baseUrl": ".",
    "paths": {
      "*": [
        "node_modules/*",
        "src/types/*"
      ]
    }
  },
  "include": [
    "src/**/*"
  ]
}

Now we're going to setup a logger and write a test for it. You could simply use console.log for logging, but with a logger you have some more functionalities at your disposal, such as disabling some logs under some circumstances (e.g. in production mode).

Create the directory for the logger

mkdir src
cd src
mkdir logger

and also the logger file and corresponding test:

backend/src/logger/logger.ts
import * as winston from 'winston';
export const log: winston.Winston = require('winston-color');
backend/src/logger/logger.test.ts
import * as mocha from 'mocha';
import * as chai from 'chai';

import {log} from './logger';

const expect = chai.expect;

describe('Logger', () => {

  it('should be able to log', () => {
    log.info('Hello');
    log.warn('World.');
    log.debug('You are');
    log.error('nice.');
  });

});

Maybe some of you ask now "but why do I need this wrapper, I could use winston directly?". Well, in my experience it almost always paid off to write a wrapper. Like this, you can switch out the implementation of the logger easily. For example when winston becomes deprecated, switching to another logger library is much simpler. Also working with typescript and IntelliJ makes this quite comfortable since writing log and then hitting alt + enter will automatically import the logger!

Now that you've finished this part, let's check if everything is working. Run

  npm test

This should result in a working test and look somewhat like this:

App Config

Our first bit of source code will be a setter and getter for the application configuration. Go to the backend/src directory and create a folder named config.

  mkdir config

Then in this, folder, we're going to create the following three files:

backend/src/config/app-properties.model.ts
export interface AppProperties {
  db: {
    host: string;
    dbname: string;
    port: number;
    dbuser: string;
    dbpassword: string;
    testsMayDropDb: boolean;
  };
}
backend/src/config/app-config.ts
import {AppProperties} from './app-properties.model';
class AppConfig {

  private _appConfig;

  // configName is the name of the properties file.
  // There's an untracked folder properties at the same level as the src directory with the properties.
  public setAppConfig(configName: string) {
    this._appConfig = require(`../../properties/${configName}.properties.json`);
  }

  public get appConfig(): AppProperties {
    return this._appConfig;
  }

}

export const appConfig = new AppConfig();
backend/src/config/app-config.test.ts
import * as mocha from 'mocha';
import * as chai from 'chai';
import {appConfig} from './app-config';

const expect = chai.expect;

describe('AppConfig', () => {

  it('should be able to set & get config', () => {
    appConfig.setAppConfig('test');
    expect(typeof appConfig.appConfig).to.equal('object');
  });

});

Finally, as you can see, the config requires a properties file. Seprating the actual config makes the code more modular and easier to read. So create a folder properties inside of the src directory. There, create two files: A local.properties.json and a test.properties.json.

They look like that:

backend/properties/local.properties.ts
{
  "db": {
    "host": "ds145220.mlab.com",
    "dbuser": "notely",
    "port": 45220,
    "dbpassword": "jlkajoijjjaksjkfiiq82",
    "dbname": "notely-local"
  }
}
backend/properties/test.properties.ts
{
  "db": {
    "host": "ds149040.mlab.com",
    "dbuser": "notely",
    "dbpassword": "jlkajoijjjaksjkfiiq82",
    "port": 49040,
    "dbname": "notely-test"
  }
}

Note: I didn't leave the passwords in there by mistake. I did it so you could get started faster with a running MongoDB. However, once you get comfortable with a running setup, please go to mlab.com (or similar) and set up your own MongoDB.

Database

Now it might be that the database setup in this project isn't the simplest possible. However, I've set it up in a way, such that it has the highest flexibility possible. Not everyone is happy with MongoDB and the way it's set up here, it shouldn't be too much of a hassle to switch to another database. At least from a source code perspective, migrating production data will of course still be painful.

Since it's a MongoDB, we'll first install the related npm packages and types

npm install --save mongodb
npm install --save-dev @types/mongodb

Connecting to the database

Afterwards, we'll have to connect to the db. That is what this class is for.

backend/src/db/database.ts
import * as mongo from 'mongodb';
import {Db} from 'mongodb';
import {log} from '../logger/logger';
import {AppProperties} from '../config/app-properties.model';

class Database {

  private _database;
  private _mongoClient;

  private mongoUri = (appParams: AppProperties) => {
    const params = appParams.db;
    return `mongodb://${params.dbuser}:${params.dbpassword}@${params.host}:${params.port}/${params.dbname}`;
  };

  constructor(

  ) {
    this._mongoClient = mongo.MongoClient;
  }

  public get database() {
    return this._database;
  }

  public connectToDatabase (appConfig: AppProperties, callback?: (database: Db) => any) {

    // Connect to the db
    this._mongoClient.connect(this.mongoUri(appConfig), (err, db) => {
      if (!err) {
        this._database = db;
        if (callback) {
          callback(db);
        }
      } else {
        log.error('Error while connecting to Database:');
        log.error(err);
      }
    });

  };

}

export const database = new Database();


backend/src/db/database.test.ts
import * as mocha from 'mocha';
import * as chai from 'chai';
import chaiHttp = require('chai-http');
import {database} from './database';
import {beforeEachDo} from '../test/before-eachs';

chai.use(chaiHttp);
const expect = chai.expect;


describe('Connect Test', () => {

  beforeEachDo.connectTestToDatabase();

  it('should be able to write to db', function(done) {

    const item = {
      text: 'Hello World'
    };

    expect(database.database !== undefined).to.be.true;
    database.database.collection('notes').insertOne(item, function(err, result) {
      expect(err).to.equal(null);
      expect(result.insertedCount).to.equal(1);
      done();
    });

  });

});

As you may have noticed, the tests import a before-eachs module we haven't created so far. So lets go ahead and create it

backend/src/test/before-eachs.ts.html
import {database} from './../db/database';
import {appConfig} from '../config/app-config';
class BeforeEach {

  public connectTestToDatabase() {
    return beforeEach('connect to db', (done) => {
      appConfig.setAppConfig('test');
      database.connectToDatabase(appConfig.appConfig, (db) => {
        db.dropDatabase().then(() => {
          done();
        });
      });
    });
  }

}

export const beforeEachDo = new BeforeEach();

This allows us to always connect to the database in the tests with a single line of code and callback free.

Again, run npm test to check if everything is running smoothly so far.

Inserting, Reading, Updating & Deleting Data

Now that we can connect to our database, we can also start writing the actual database layer that inserts and extracts data to- and from the database. We will call this layer the "Database Access Object" or short "dao". Hence, the following files

backend/src/db/dao.ts
import * as mongo from 'mongodb';
import {database} from './database';
import {log} from '../logger/logger';
import {DatabaseError, DatabaseResponse} from './database-response.model';
import {Cursor, MongoCallback, MongoClient, MongoError} from 'mongodb';
import {utils} from '../utils/utils';

// Database Access Object
// Everything that operates directly on the database goes here
// i.e. everything that has to do anything with mongodb
// goal is to abstract away MongoDB stuff and localize in one place, so if you want to swap e.g. for a relational DB
// it's not too much effort

// also, don't expose Mongo API directly, but program against an interface (DatabaseResponse)

export namespace dao {

  export function read(id: string, collectionName: string, cb: (dbResponse: DatabaseResponse) => void): void {
    database.database.collection(collectionName, (err, collection) => {

      if (err) {
        cb({
          error: err
        });
      } else {

        collection.findOne({'_id': new mongo.ObjectID(id)}, (innerError, data) => {

          if (innerError) {
            cb({
              error: innerError
            });
          } else {
            if (data) {
              cb({
                error: null,
                data: morphDataOnRetrieval(data)
              });
            } else {
              cb({
                error: {
                  message: 'not found'
                }
              });
            }
          }
        });

      }

    });
  }


  export function readAll(collectionName: string, cb: (dbResponse: DatabaseResponse) => void): void {
    database.database.collection(collectionName, (err, collection) => {

      if (err) {
        cb({
          error: err
        });
      } else {

        collection.find({}, (innerError, cursor) => {

          if (innerError) {
            cb({
              error: innerError
            });
          } else {
            if (cursor) {
              cursor.toArray().then(ary => {
                cb({
                  error: null,
                  data: morphDataOnRetrieval(ary)
                });
              });

            } else {
              cb({
                error: {
                  message: 'not found'
                }
              });
            }
          }
        });

      }

    });
  }

  export function readOneByField(fieldName: string, fieldValue: string, collectionName: string, cb: (dbResponse: DatabaseResponse) => void): void {
    database.database.collection(collectionName, (err, collection) => {

      if (err) {
        cb({
          error: err
        });
      } else {

        const searchObject = {};
        searchObject[fieldName] = fieldValue;

        collection.findOne(searchObject, (innerError, data) => {
          if (innerError) {
            cb({
              error: innerError
            });
          } else {
            if (data) {
              cb({
                error: null,
                data: morphDataOnRetrieval(data)
              });
            } else {
              cb({
                error: {
                  message: 'not found'
                }
              });
            }
          }
        });
      }

    });
  }


  export function create(item: Object, collectionName: string, cb: (dbResp: DatabaseResponse) => void): void {

    // deep copy object so input doesn't get mutated
    const itemCopy = utils.deepCopyData(item);

    database.database.collection(collectionName, (err: MongoError, collection) => {
      if (err) {
        cb({
          error: mongoErrorToGeneralDbError(err)
        });
      } else {
        collection.insertOne(itemCopy, (innerError: MongoError, result) => {
          if (innerError) {
            cb({
              error: mongoErrorToGeneralDbError(innerError)
            });
          } else {
            cb({
              error: null,
              data: morphDataOnRetrieval(itemCopy)
            });
          }
        });
      }
    });
  }


  export function update(item, collectionName: string, cb: (dbResp: DatabaseResponse) => void): void {

    // deep copy object so input doesn't get mutated and morph it to correct storage form
    const itemCopy = morphDataOnStorage(item);

    database.database.collection(collectionName, (err, collection) => {
      if (err) {
        cb({
          error: mongoErrorToGeneralDbError(err)
        });
      } else {
        collection.updateOne({'_id': new mongo.ObjectID(itemCopy._id)}, item, (innerError: MongoError, result) => {
          if (innerError) {
            cb({
              error: mongoErrorToGeneralDbError(innerError)
            });
          } else {
            cb({
              error: null,
              data: morphDataOnRetrieval(itemCopy)
            });
          }
        });
      }
    });
  }


  export function remove(id: string, collectionName: string, cb: (dbResp: DatabaseResponse) => void): void {
    database.database.collection(collectionName, (err, collection) => {
      if (err) {
        cb({
          error: mongoErrorToGeneralDbError(err)
        });
      } else {
        collection.deleteOne({'_id': new mongo.ObjectID(id)}, (innerError, result) => {
          if (innerError) {
            cb({
              error: mongoErrorToGeneralDbError(innerError)
            });
          } else {
            cb({
              error: null
            });
          }
        });
      }
    });
  }


  function mongoErrorToGeneralDbError (err: MongoError): DatabaseError {
    return {
      message: err.message
    };
  }

  function morphDataOnRetrieval(data, logme?: boolean) {

    if (!data) {
      log.error('No data!');
      return;
    }

    const dataCopy = utils.deepCopyData(data);

    const morphResource = (resource): void => {
      if (typeof resource._id !== 'string') {
        resource.uid = resource._id.toHexString();
      } else {
        resource.uid = resource._id;
      }
      delete resource._id;
    };

    if (Array.isArray(dataCopy)) {
      dataCopy.forEach(resource => {
        morphResource(resource);
      });
    } else {
      morphResource(dataCopy);
    }

    return dataCopy;
  };

  function morphDataOnStorage(data) {
    const dataCopy = utils.deepCopyData(data);
    dataCopy._id = data.uid;
    delete dataCopy.uid;
    return dataCopy;
  };

}
backend/src/db/dao.test.ts
import * as mocha from 'mocha';
import * as chai from 'chai';
import {dao} from './dao';
import {beforeEachDo} from '../test/before-eachs';
import {DatabaseResponse} from './database-response.model';
const expect = chai.expect;

describe('DAO', () => {

  beforeEachDo.connectTestToDatabase();

  // TODO: remove unnecessary nesting
  it('should be able to insert, read, update, delete', function(done) {

    const item = {text: 'hello'};

    const doDelete = (id: string) => {
      dao.remove(id, 'items', (dbResponse: DatabaseResponse) => {
        expect(dbResponse.error).to.equal(null);
        done();
      });
    };

    const doUpdate = (updateItem) => {
      updateItem.text = updateItem.text + ' world!';

      dao.update(updateItem, 'items', (dbResp) => {

        expect(dbResp.error).to.equal(null);

        const doReadTwo = (id: string) => {
          dao.read(id, 'items', (dbResp2) => {
            expect(dbResp2.error).to.equal(null);
            expect(dbResp2.data.text).to.equal('hello world!');
            doDelete(updateItem.uid);
          });
        };
        doReadTwo(updateItem.uid);

      });
    };

    const doRead = (uid: string) => {
      dao.read(uid, 'items', (dbResponse: DatabaseResponse) => {

        expect(dbResponse.error).to.equal(null);
        expect(dbResponse.data.text).to.equal('hello');
        expect(dbResponse.data.uid).to.exist;

        doUpdate(dbResponse.data);

      });
    };


    const doCreate = () => {
      dao.create(item, 'items', (dbResp) => {
        expect(dbResp.error).to.equal(null);
        doRead(dbResp.data.uid);
      });
    };

    const start = () => {
      doCreate();
    };

    start();

  });

  it('should be able to readAll', function(done) {

    const item = {hello: 'world'};
    const item2 = {hello: 'world'};


    dao.create(item, 'items', (dbResp) => {
      dao.create(item, 'items', (dbResp2) => {
        dao.readAll('items', dbResp3 => {
          expect(dbResp3.error).to.be.null;
          expect(Array.isArray(dbResp3.data)).to.be.true;
          expect(dbResp3.data.length).to.equal(2);
          done();
        });
      });
    });

  });

});
backend/src/db/database-response.model.ts
export interface DatabaseResponse {
  error: DatabaseError;
  data?: any;
}

export interface DatabaseError {
  code?: number;
  message: string;
}

In order to make this work, we also have to create a src/utils/utils file that looks like so

backend/src/utils/utils.ts
export namespace utils {

  /* Copies the data, but loses function assignments! */
  export function deepCopyData(data: Object) {
    return JSON.parse(JSON.stringify(data));
  }

}

It's quite a lot of code, but don't get intimidated, it's actually not that complex. What you have to know to understand it is the following:

I know this bloats the code a bit, but it safeguards you against getting locked into something you don't want to. Again you can test if everything's running so far with npm test.

Router

Next up is the router. This is where our application actually starts to do some stuff via HTTP endpoints. The router of choice in the MEAN Stack is Express (the E in mEan). ExpressJS is a standard choice for most web-apps with a node powered backend. We'll need to install express and some other packages to get the code in this section running.

npm install --save express body-parser
npm install --save-dev @types/express @types/body-parser

Getting started with Express

In order to get started, we'll host a simple welcome file. First we create the router directory. In src,

mkdir router
cd router
mkdir endpoints

Then create the following files:

backend/src/router/router.ts
404: Not Found
backend/src/router/router.test.ts
import * as mocha from 'mocha';
import * as chai from 'chai';
import chaiHttp = require('chai-http');

chai.use(chaiHttp);
const expect = chai.expect;

describe('Router Test', () => {

  /* not valid anymore with frontend
  it('should be json', () => {
    return chai.request(router).get('/')
        .then(res => {
          expect(res.type).to.eql('application/json');
        });
  });

  it('should have a message prop', () => {
    return chai.request(router).get('/')
        .then(res => {
          expect(res.body.message).to.eql('Hello World!');
        });
  });
  */

});
backend/src/router/endpoints/welcome-html-router.ts
import {Router, Request, Response, NextFunction} from 'express';

export class WelcomeHtmlRouter {

  router: Router;

  /**
   * Attach handler to endpoint.
   */
  init() {
    this.router.get('/', this.welcome);
  }

  /**
   * Initialize the WelcomeHtmlRouter
   */
  constructor() {
    this.router = Router();
    this.init();
  }

  public welcome(req: Request, res: Response, next: NextFunction) {
    res.status(200)
        .send(`<html><head><title>My Title</title></head><body><p>Welcome!</p></body></html>`);
  }


}

// Create the SecondRouter, and export its configured Express.Router
const intialRouter = new WelcomeHtmlRouter();
intialRouter.init();

export const welcomeHtmlRouter = intialRouter.router;
backend/src/router/endpoints/welcome-html-router.test.ts
import * as mocha from 'mocha';
import * as chai from 'chai';
import chaiHttp = require('chai-http');
import {router} from '../router';

chai.use(chaiHttp);
const expect = chai.expect;

describe('Test simple welcome Html Router', () => {

  it('should return html containing the word welcome', (done) => {
    chai.request(router)
        .get(`/welcome`)
        .then((resp) => {
          expect((<any>resp).text).to.contain('Welcome!');
          done();
        });
  });

});

We should be able to run npm test successfully now. So far, we didn't even bother to open the browser, but we're actually at a point where we could almost do so, since we now have our first endpoint. Before we do that, however, we're missing one more file: The index.ts. If you were to run npm start, it wouldn't have done anything but throw an error up until now. But this is going to change. At the src level of the backend, create the following file

backend/src/index.ts
import * as http from 'http';
import {router} from './router/router';
import {database} from './db/database';
import {log} from './logger/logger';
import {appConfig} from './config/app-config';

// Step 1) Set & Get App Configuration
appConfig.setAppConfig(process.argv[2] || 'local');


// Step 2) Connect to the database
database.connectToDatabase(appConfig.appConfig, (db) => {

  // when connected to db:

  // Step 3) Set Port for router
  const normalizePort = (val: number|string): number|string|boolean => {
    const port: number = (typeof val === 'string') ? parseInt(val, 10) : val;
    if (isNaN(port)) {
      return val;
    } else if (port >= 0) {
      return port;
    } else {
      return false;
    }
  };

  const port = normalizePort(process.env.PORT || 4242);
  router.set('port', port);
  const server = http.createServer(router);

  // Step 4) Handle Errors
  const onError = (error: NodeJS.ErrnoException): void => {
    if (error.syscall !== 'listen') {
      throw error;
    }
    const bind = (typeof port === 'string') ? 'Pipe ' + port : 'Port ' + port;
    switch (error.code) {
      case 'EACCES':
        console.error(`${bind} requires elevated privileges`);
        process.exit(1);
        break;
      case 'EADDRINUSE':
        console.error(`${bind} is already in use`);
        process.exit(1);
        break;
      default:
        throw error;
    }
  };
  const onListening = (): void => {
    const addr = server.address();
    const bind = (typeof addr === 'string') ? `pipe ${addr}` : `port ${addr.port}`;
  };
  server.on('error', onError);
  server.on('listening', onListening);

  server.listen(port, function(){
    log.info('Server listening at port %d', port);
  });

});

This will enable us to finally get a running backend at port 3000 using the npm start command. After npm start, you can check the welcome file at http://localhost:3000/welcome.

Adding a generic CRUD endpoint

Now to add some real functionality, next we're adding a CRUD (create, read, update, delete) router endpoint. This endpoint will actually write to- and retrieve from the database, leveraging the previously built database layer.

backend/src/router/endpoints/simple-crud-router.ts
import {Router, Request, Response, NextFunction} from 'express';
import {dao} from '../../db/dao';
import {api} from '../api';

export class SimpleCrudRouter {
  router: Router;

  /**
   * Take each handler, and attach to one of the Express.Router's
   * endpoints.
   */
  init() {
    this.router.post('/:resource', this.create);
    this.router.get('/:resource', this.getAll);
    this.router.get('/:resource/:id', this.getOne);
    this.router.put('/:resource', this.updateOne);
    this.router.delete('/:resource/:id', this.deleteOne);
  }

  /**
   * Initialize the CrudRouter
   */
  constructor() {
    this.router = Router();
    this.init();
  }

  /**
   * CREATE one resource
   */
  public create(req: Request, res: Response, next: NextFunction) {

    const resource = req.body;
    const resourceName = req.params.resource;

    dao.create(resource, resourceName, (dbResp) => {
      if (dbResp.error) {
        res.status(500).send({
          message: 'Server error',
          status: res.status
        });
      } else {
        res.status(201)
          res.location(`${api.root()}/${resourceName}/${dbResp.data.insertId}`)
            .send({
              message: 'Success',
              status: res.status,
              data: dbResp.data
            });
      }
    });

  }

  /**
   * GET one resource by id
   */
  public getOne(req: Request, res: Response, next: NextFunction) {
    const resourceId = req.params.id;
    const resourceName = req.params.resource;

    dao.read(resourceId, resourceName, (dbResp) => {
      if (dbResp.error) {
        res.status(500).send({
          message: 'Server error',
          status: res.status
        });
      } else {
        res.status(200)
            .send({
              message: 'Success',
              status: res.status,
              data: dbResp.data
            });
      }
    });
  }

  /**
   * UPDATE one resource by id
   */
  public updateOne(req: Request, res: Response, next: NextFunction) {
    const resourceName = req.params.resource;

    dao.update(req.body, resourceName, (dbResp) => {

      if (dbResp.error) {
        res.status(500).send({
          message: `Database error:(${dbResp.error.code}) ${dbResp.error.message}`,
          status: res.status
        });
      } else {
        res.status(200)
            .send({
              message: 'Success',
              status: res.status,
              data: dbResp.data
            });
      }


    });
  }


  /**
   * GET all Resources.
   */
  public getAll(req: Request, res: Response, next: NextFunction) {
    const resourceName = req.params.resource;

    dao.readAll(resourceName, (dbResp) => {
      if (dbResp.error) {
        res.status(500).send({
          message: 'Server error',
          status: res.status
        });
      } else {
        res.status(200)
            .send({
              message: 'Success',
              status: res.status,
              data: dbResp.data
            });
      }
    });
  }

  /**
   * DELETE one resource by id
   */
  public deleteOne(req: Request, res: Response, next: NextFunction) {
    const resourceId = req.params.id;
    const resourceName = req.params.resource;

    dao.remove(resourceId, resourceName, (dbResp) => {
      if (dbResp.error) {
        res.status(500).send({
          message: 'Server error',
          status: res.status
        });
      } else {
        res.status(200)
            .send({
              message: 'Success',
              status: res.status
            });
      }
    });
  }


}

// Create the CrudRouter, and export its configured Express.Router
const intialRouter = new SimpleCrudRouter();
intialRouter.init();

export const simpleCrudRouter = intialRouter.router;
backend/src/router/endpoints/simple-crud-router.test.ts
import * as mocha from 'mocha';
import * as chai from 'chai';
import chaiHttp = require('chai-http');
import {router} from '../router';
import {beforeEachDo} from '../../test/before-eachs';
import {log} from '../../logger/logger';
import * as assert from 'assert';
import {dao} from '../../db/dao';

chai.use(chaiHttp);
const expect = chai.expect;

describe('Simple CRUD Route Test', () => {

  beforeEachDo.connectTestToDatabase();

  it('should return the item', (done) => {
    dao.create({'hello': 'world'}, 'items', (dbResp) => {
      chai.request(router).get(`/api/v1/items/${dbResp.data.uid}`)
          .end((err, res) => {
            expect(res).to.have.status(200);
            expect(res.body.data.hello).to.equal('world');
            done();
          });
    });
  });

  it('should return all items', (done) => {
    dao.create({'hello': 'world'}, 'items', (dbResp1) => {
      dao.create({'goodbye': 'world'}, 'items', (dbResp2) => {
        chai.request(router).get(`/api/v1/items`)
            .end((err, res) => {
              expect(res).to.have.status(200);
              expect(Array.isArray(res.body.data)).to.be.true;
              expect(res.body.data.length).to.equal(2);
              done();
            });
      });
    });
  });

  it('should work with promise', (done) => {
    dao.create({'hello': 'world'}, 'items', (dbResp) => {
      chai.request(router).get(`/api/v1/items/${dbResp.data.uid}`)
          .then(res => {
            expect(res).to.have.status(200);
            expect(res.body.data.hello).to.equal('world');
            done();
          }, err => {
            done();
          })
          .catch(function (err) {
            throw err;
          });
    });
  });


  it('should be able to create', (done) => {
    chai.request(router)
        .post(`/api/v1/items`)
        .send({
          'hair': 'red',
          'nose': 'long'
        })
        .then(res => {
          expect(res).to.have.status(201);
          expect(res.body.data.hair).to.equal('red');
          expect(res.body.data.uid).to.exist;
          done();
        }, err => {
          log.error('Error on POST request:');
          log.error(err);
          done();
        })
        .catch(function (err) {
          throw err;
        });
  });

  it('should be able to update', (done) => {
    const item: any = {'hello': 'world'};
    dao.create(item, 'items', (dbResp) => {
      dbResp.data.hello = 'planet';
      chai.request(router)
          .put(`/api/v1/items`)
          .send(dbResp.data)
          .then(res => {
            expect(res).to.have.status(200);
            chai.request(router).get(`/api/v1/items/${dbResp.data.uid}`).then((res2) => {
              expect(res2.body.data.hello).to.equal('planet');
              done();
            }, () => {
              log.error('Error on GET request');
              done();
            });
          }, err => {
            log.error('Error on PUT request:');
            log.error(err);
            done();
          })
          .catch(function (err) {
            throw err;
          });
    });
  });


  it('should be able to delete', (done) => {
    dao.create({'hello': 'world'}, 'items', (dbResp) => {
      chai.request(router)
          .del(`/api/v1/items/${dbResp.data.uid}`)
          .then(res => {
            expect(res).to.have.status(200);
            chai.request(router).get(`/api/v1/items/${dbResp.data.uid}`).then(() => {
              // shouldnt find anything
              assert(false);
            }, () => {
              // TODO: make this a 404
              // expect(res.status).to.equal(500);
              done();
            });
          }, err => {
            done();
          })
          .catch(function (err) {
            throw err;
          });
    });
  });


});

In order to activate those routes, we also need to register them. To do so, open the router.ts file and uncomment the line

this.express.use('/api/v1/', simpleCrudRouter);

right after this.express.use('/welcome', welcomeHtmlRouter);. In case you're using an IDE you probably noticed by now: we also need to import the module. At the top of router.ts, also uncomment

import {simpleCrudRouter} from "./endpoints/simple-crud-router";

Now you should again have running tests (npm test).

Authentication

We have already achieved a lot. We've set up and configured an application that can read & write to MongoDB and which can be talked to over a RESTful API. However, usually we don't allow anybody everything. So in this last step for the backend, we're going to add authentication.

Since we never ever store plain text passwords in our database, as a first and independent step we can start to set up a password encryption algorithm. A good choice for an encryption algorithm for passwords is bcrypt. Luckily there's a npm module implementing this algorithm, so we just have to wire things together. Let's first install the npm module.

npm install --save bcrypt-nodejs
npm install --save-dev @types/bcrypt-nodejs
backend/src/auth/password-cryptograher.ts
import * as bcrypt from 'bcrypt-nodejs';
export namespace passwordCryptographer {

  function saltRounds() {
    return 5;
  }

  export function doHash (plaintextPassword: string): Promise<string> {

    return new Promise(function(resolve, reject) {

      bcrypt.genSalt(saltRounds(), (error, salt) => {
        bcrypt.hash(plaintextPassword, salt, null, function(err, hash) {
          if (err) {
            reject(err);
          } else {
            resolve(hash);
          }
        });
      });

    });

  }

  export function doCompare (plaintextPassword, hash): Promise<boolean> {

    return new Promise(function(resolve, reject) {

      bcrypt.compare(plaintextPassword, hash, function(err, res) {
        if (err) {
          reject(err);
        } else {
          resolve(res);
        }
      });

    });

  }

}
backend/src/auth/password-cryptograher.test.ts
import * as mocha from 'mocha';
import * as chai from 'chai';

import {log} from '../logger/logger';
import {passwordCryptographer} from './password-cryptographer';
import * as bcrypt from 'bcrypt-nodejs';
const expect = chai.expect;

describe('bcrypt', () => {

  it('should be able to encrypt & decrypt', (done) => {

    const mypw = 'Hello World';

    passwordCryptographer.doHash(mypw).then(encrypted => {
      passwordCryptographer.doCompare(mypw, encrypted).then((isMatching: boolean) => {
        expect(isMatching).to.equal(true);
        done();
      }, (err) => {
        log.error('Error while comparing:');
        log.error(err);
      });
    }, (err) => {
      log.error('Error while encrypting:');
      log.error(err);
    });

  });

  it('shouldnt match wrong passwords', (done) => {

    const mypw = 'Hello World';

    passwordCryptographer.doHash(mypw).then(encrypted => {
      passwordCryptographer.doCompare(mypw + ' is wrong', encrypted).then((isMatching: boolean) => {
        expect(isMatching).to.equal(false);
        done();
      }, (err) => {
        log.error('Error while comparing:');
        log.error(err);
      });
    }, (err) => {
      log.error('Error while encrypting:');
      log.error(err);
    });

  });

});

Sidenote: This is a perfect example of where "programming against an interface, not against an implementation" was helpful. I first used the bcrypt package and now I'm using the bcrypt-nodejs package because bcrypt threw crazy errors all of the time. Now, because I wrapped the original bcrypt in a thin layer, I could simply switch out the implementation! I only had to change one file to switch out the entire library. Of course, the new library had a bit a different interface than the old library. The old library was promise based and the new one callback based. But by converting the callbacks into a promise in the wrapping layer, I didn't have to change the interface used in the app!

We're starting to be at an application size where it's annoying to always have to run all unit tests. In the package.json we've already set a method to run a single test. So in order to check if bcrypt is working, run TEST=bcrypt npm run spec. Like this, every describe or it containing the sequence "bcrypt" is run.

What we now can do building on the password cryptographer is creating users and storing their passwords as hashed values. In the db directory, create the UserDAO and its corresponding test and model.

backend/src/db/user-dao.ts
import * as mongo from 'mongodb';
import {database} from './database';
import {log} from '../logger/logger';
import {DatabaseResponse} from './database-response.model';
import {dao} from './dao';
import {passwordCryptographer} from '../auth/password-cryptographer';
import {User} from './user.model';
import {utils} from '../utils/utils';

export namespace userDAO {

  export function create(user: User, password: string, cb: (dbResponse: DatabaseResponse) => void) {

    const userCopy = utils.deepCopyData(user);

    dao.readOneByField('email', userCopy.email, 'users', (dbResp) => {

      // Condition to create a new is user is no user with this email exists
      // This means that a database error is actually what you expect when creating a new user!
      if (dbResp.error) {

        passwordCryptographer.doHash(password).then((hash: string) => {
          userCopy.password = {
            hash: hash,
            algorithm: 'bcrypt'
          };
          dao.create(userCopy, 'users', cb);
        }, (err) => {
          log.error(err);
          return cb({
            error: {
              message: 'Problem during hashing'
            }
          });
        });

      } else {
        // if a user with this email exists, deny creation
        return cb({
          error: {
            message: 'User already exists'
          }
        });
      }
    });

  }

  export function getByMail(email: string, cb: (dbResponse: DatabaseResponse) => void) {
    dao.readOneByField('email', email, 'Users', cb);
  }


  export function getById(id: string, cb: (dbResponse: DatabaseResponse) => void) {
    dao.read(id, 'Users', cb);
  }

}
backend/src/db/user-dao.test.ts
import * as mocha from 'mocha';
import * as chai from 'chai';
import {beforeEachDo} from '../test/before-eachs';
import {userDAO} from './user-dao';
import {log} from '../logger/logger';
import {User} from './user.model';
const expect = chai.expect;

describe('UserDAO', () => {

  beforeEachDo.connectTestToDatabase();

  it('should be able to create user (only once)', function(done) {

    const user: User = {
      email: 'hans'
    };

    userDAO.create(user, '1234', (dbResponse) => {
      expect(dbResponse.error).to.be.null;
      expect(dbResponse.data.uid).to.exist;

      userDAO.create(user, '1234', (innerDbResponse) => {
        expect(innerDbResponse.error).to.exist;
        expect(innerDbResponse.error.message).to.equal('User already exists');
        done();
      });

    });


  });

});
backend/src/db/user.model.ts
export interface User {
  uid?: string;
  email: string;
  password?: {
    hash: string;
    algorithm: HashingAlgorithm;
  };
}

type HashingAlgorithm = 'bcrypt';

So the UserDAO can create a new user if and only if no user with this e-mail already exists. This test can be ran with TEST=user npm run spec. You might also ask, why the "hashing-algorithm" is stored on the user. This isn't strictly necessary, but in case you're running in production and want to switch the hashing algorithm, you could upload the new algorithm and run the two algorithms parallel while you have time to migrate the old hashed values.

Last but not least we want to be able to authenticate the user, meaning that when he or she tries to log in providing username / email and password we can return success or fail. This is done with yet another npm module, namely passport. To install it, run

npm install --save passport passport-local
npm install --save-dev @types/passport @types/passport-local

What is passport good for? In short, you can add different authentication methods to endpoints easily. It also has a lot of different pre-implemented methods, "strategies" as they call them, for example for Google+ or Facebook authentication. We'll start with a "local strategy" meaning an authentication specific to the application. So let's set up this local strategy.

backend/src/auth/passport.ts
import * as passport from 'passport';
import * as local from 'passport-local';
import {dao} from '../db/dao';
import {passwordCryptographer} from './password-cryptographer';
import {User} from '../db/user.model';
import * as expressSession from 'express-session';


export namespace passportInit {

  function initializePassportLocalStrategy(): boolean {
    const updatedPassport = passport.use('local', new local.Strategy({
        // by default, local strategy uses username and password, we will override with email
        usernameField : 'email',
        passwordField : 'password',
      },
      function(email, password, done) {

        dao.readOneByField('email', email, 'users', function (dbResp) {
          if (dbResp.error) {
            // It's better not to disclose whether username OR password is wrong
            return done(null, false, { message: 'Wrong password or username.' });
          } else if (!dbResp.data) {
            return done(null, false, { message: 'Wrong password or username.' });
          } else {
            passwordCryptographer.doCompare(password, dbResp.data.password.hash).then(isMatching => {
              if (!isMatching) {
                return done(null, false, { message: 'Wrong password or username.' });
              } else {
                return done(null, dbResp.data);
              }
            });
          }
        });
      }
    ));
    return updatedPassport ? true : false;
  }


  export function init(appRouter): string {
    appRouter.use(passport.initialize());
    initializePassportLocalStrategy();
    return 'success';
  }

}
backend/src/auth/passport.test.ts
import * as mocha from 'mocha';
import * as chai from 'chai';
import {passportInit} from './passport';
import * as express from 'express';

const expect = chai.expect;

describe('MyPassport', () => {

  it('should have registered the local strategy', () => {
    expect(passportInit.init(express())).to.equal('success');
  });

});

As you can see we're leveraging the password-encryption previously wired up to check if the provided plaintext password matches the hash. Once this strategy is set up, we can use it in our endpoints. Here, we're going to implement a Login endpoint. It looks like this

backend/src/router/endpoints/login-router.ts
import {Router, Request, Response, NextFunction} from 'express';
import * as passport from 'passport';

export class LoginRouter {
  router: Router;

  /**
   * Take login handler and attach to login endpoint, but precede it with authentication
   */
  init() {
    this.router.post('/login', passport.authenticate(
      'local',
      {
        session: false,
        failWithError: true
      }),
      this.loginHandler,
      this.errorHandler);
  }

  /**
   * Initialize the login
   */
  constructor() {
    this.router = Router();
    this.init();
  }

  public loginHandler(req: Request, res: Response, next: NextFunction) {
    // If this function gets called, authentication was successful.
    // `req.user` contains the authenticated user.

    res.status(200).send({
      message: 'Success',
      status: res.status,
      data: req.user
    });

  }

  errorHandler(err, req, res, next) {
    console.log('handling error');
    res.statusMessage = 'Wrong username or password.';
    res.status(err.status).send();
  }



}

// Create the CrudRouter, and export its configured Express.Router
const intialRouter = new LoginRouter();
intialRouter.init();

export const loginRouter = intialRouter.router;
backend/src/router/endpoints/login-router.test.ts
import * as mocha from 'mocha';
import * as chai from 'chai';
import chaiHttp = require('chai-http');
import {router} from '../router';
import {beforeEachDo} from '../../test/before-eachs';
import {log} from '../../logger/logger';
import * as assert from 'assert';
import {User} from '../../db/user.model';
import {userDAO} from '../../db/user-dao';

chai.use(chaiHttp);
const expect = chai.expect;

describe('LoginRouter', () => {

  beforeEachDo.connectTestToDatabase();

  it('should be able to login', (done) => {

    const user: User = {
      email: 'hans'
    };

    const plaintextPassword = 'Hello World';

    userDAO.create(user, plaintextPassword, (dbResp) => {

      expect(dbResp.error).to.be.null;

      chai.request(router)
          .post(`/api/v1/login`)
          .send({
            email: user.email,
            password: plaintextPassword
          })
          .then((resp: any) => {
            expect(resp.body.data.uid).to.equal(dbResp.data.uid);
            done();
          }, (err) => {
            log.error(err);
            assert(false);
            done();
          })
          .catch((err) => {
            throw err;
          });

    });

  });

  it('shouldnt be able to login with wrong password', (done) => {

    const user: User = {
      email: 'hans'
    };

    const plaintextPassword = 'Hello World';

    userDAO.create(user, plaintextPassword, (dbResp) => {

      expect(dbResp.error).to.be.null;

      chai.request(router)
          .post('/api/v1/login')
          .send({
            email: user.email,
            password: 'some wrong password'
          })
          .catch((err) => {
            expect(err.response.res.statusCode).to.equal(401);
            done();
          });
    });

  });

});

If you were to run TEST=login npm run spec now it would still fail. Why? We yet have to register this new route in the router. So open router.ts and add the following line

this.express.use('/api/v1/', loginRouter);

above the simpleCrudRouter. You'll have to add it above, since the simpleCrudRouter already occupies the endpoint /api/v1/login, but express tries to match from top to bottom. Also, don't forget the import at the top

import {loginRouter} from "./endpoints/login-router";

Wow okay, that's it, we're pretty much at the end of our backend journey. You should be able to run npm test and have all running tests. Furthermore you should be able to start the server with npm start, so you have a running backend which you can implement the frontend against. Which is what we're going to do next.

Frontend

The Angular Cli does a good job at scaffolding your new web-app. However, I have still built a small frontend to illustrate how you would use it together with the backend we've built so far. Also I'd like to illustrate some points with this frontend about my views on things like state-management and a good directory setup. Last but not least, it's nice to have an complete working app, so you don't have to build things like login etc. yourself.

Since the project is close to most Angular Tutorials out there, I'm just pointing out here what's different about the frontend.

I think those are the most important differences to point out. You can check out the full code at: https://github.com/bersling/typescript-angular-seed

Well, that's everything I have to say for now! If you want to stay posted, make sure to subscribe to the Github repository!