Tutorial Building CRUD App from Scratch using MEAN Stack (Angular 2)

by Didin J. on Mar 20, 2017 Tutorial Building CRUD App from Scratch using MEAN Stack (Angular 2)

Step by step tutorial building CRUD app from scratch using MEAN Stack (MongoDB, Express, Angular 2 and Node.js).

MEAN is stand for Mongo, Express, Angular 2 and Node.js. This combination is popular in web application development. There is some MEAN stack bundle that ready use like MEAN.js and MEAN.io, but we are not using that ready to use the framework. We will do it from scratch creating simple CRUD app, using existing tools and generator that comes with Angular 2.

Before we get started, there are few things required for building this MEAN stack web application.

- Node.js
- Express.js 
- Angular 2 CLI

Using all tools above, we will create simple CRUD (Create-Read-Update-Delete) web application.


1. Create Angular 2 Application using Angular CLI

We assume that you already install Node.js. We will use Angular CLI to create new Angular 2 application. Open terminal (Mac/Linux) or Node.js command prompt (Windows) then type this command to install Angular CLI.

sudo npm install @angular/cli -g

Now, type this command to create new Angular 2 application.

ng new mean-app

Go to the newly created application folder.

cd mean-app

Run the Angular 2 application by typing this command.

ng serve

If you see "Webpack. Compiled Successfully" in the terminal then open the browser then point to this URL "http://localhost:4200". You will see this page if everything working properly.

Tutorial Building CRUD App From Scratch using MEAN Stack (Angular 2) - Angular Home Page

 

2. Add Express.js Server Functionality

This time will different than previous Node.js and Express.js tutorial. We will create required files for backend manually instead of generating new Express.js application. First, we install Express.js and Mongoose.js with their required dependencies. Just type this command.

npm install --save express body-parser morgan body-parser serve-favicon

Then, add bin folder and www file inside bin folder.

mkdir bin
touch bin/www

Open and edit www file then add this lines of codes.

#!/usr/bin/env node

/**
 * Module dependencies.
 */

var app = require('../app');
var debug = require('debug')('mean-app:server');
var http = require('http');

/**
 * Get port from environment and store in Express.
 */

var port = normalizePort(process.env.PORT || '3000');
app.set('port', port);

/**
 * Create HTTP server.
 */

var server = http.createServer(app);

/**
 * Listen on provided port, on all network interfaces.
 */

server.listen(port);
server.on('error', onError);
server.on('listening', onListening);

/**
 * Normalize a port into a number, string, or false.
 */

function normalizePort(val) {
  var port = parseInt(val, 10);

  if (isNaN(port)) {
    // named pipe
    return val;
  }

  if (port >= 0) {
    // port number
    return port;
  }

  return false;
}

/**
 * Event listener for HTTP server "error" event.
 */

function onError(error) {
  if (error.syscall !== 'listen') {
    throw error;
  }

  var bind = typeof port === 'string'
    ? 'Pipe ' + port
    : 'Port ' + port;

  // handle specific listen errors with friendly messages
  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;
  }
}

/**
 * Event listener for HTTP server "listening" event.
 */

function onListening() {
  var addr = server.address();
  var bind = typeof addr === 'string'
    ? 'pipe ' + addr
    : 'port ' + addr.port;
  debug('Listening on ' + bind);
}

To make the server run from bin/www, open and edit "package.json" then replace "start" value.

"scripts": {
  "ng": "ng",
  "start": "node ./bin/www",
  "test": "ng test",
  "pree2e": "webdriver-manager update --standalone false --gecko false",
  "e2e": "protractor"
},

Now, create app.js in the root of project folder.

touch app.js

Open and edit app.js then add all this lines of codes.

var express = require('express');
var path = require('path');
var favicon = require('serve-favicon');
var logger = require('morgan');
var bodyParser = require('body-parser');

var book = require('./routes/book');
var app = express();

app.use(logger('dev'));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({'extended':'false'}));
app.use(express.static(path.join(__dirname, 'dist')));

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

// catch 404 and forward to error handler
app.use(function(req, res, next) {
  var err = new Error('Not Found');
  err.status = 404;
  next(err);
});

// error handler
app.use(function(err, req, res, next) {
  // set locals, only providing error in development
  res.locals.message = err.message;
  res.locals.error = req.app.get('env') === 'development' ? err : {};

  // render the error page
  res.status(err.status || 500);
  res.render('error');
});

module.exports = app;

Next, create routes folder then create routes file for the book.

mkdir routes
touch routes/book.js

Open and edit "book.js" file then add this lines of codes.

var express = require('express');
var router = express.Router();

/* GET home page. */
router.get('/', function(req, res, next) {
  res.send('Express REST API');
});

module.exports = router;

Because we point Express static path to "dist" directory which it is Angular build folder. For that, build Angular application first.

ng build

Now, run the server using this command.

nodemon

or

npm start

Open your browser and point to "http://localhost:3000". You will see the same page as running Angular application.

Tutorial Building CRUD App from Scratch using MEAN Stack (Angular 2) - Angular View

When you change to this URL "http://localhost:3000/book", it will show Express page.

Tutorial Building CRUD App from Scratch using MEAN Stack (Angular 2) - Express View

 


3. Create REST API for Accessing Book Data

To create the REST API first thing to do is install Mongoose.js for creating a model.

npm install mongoose --save

Open and edit app.js on the root of the project. Add require line for Mongoose.

var mongoose = require('mongoose');

Create a connection to MongoDB database. Add this lines of codes after the require section.

mongoose.Promise = global.Promise;

mongoose.connect('mongodb://localhost/mean-app')
  .then(() =>  console.log('connection successful'))
  .catch((err) => console.error(err));

Create model folder and file for building Mongoose Model Schema.

mkdir models
touch models/Book.js

Open and edit Book.js file then add this lines of codes.

var mongoose = require('mongoose');

var BookSchema = new mongoose.Schema({
  isbn: String,
  title: String,
  author: String,
  publisher: String,
  published_date: Date,
  price: Number,
  updated_at: { type: Date, default: Date.now },
});

module.exports = mongoose.model('Book', BookSchema);

Open and edit previously created file routes/book.js then replace all codes with this.

var express = require('express');
var router = express.Router();
var mongoose = require('mongoose');
var Book = require('../models/Book.js');

/* GET ALL BOOKS */
router.get('/', function(req, res, next) {
  Book.find(function (err, products) {
    if (err) return next(err);
    res.json(products);
  });
});

/* GET SINGLE BOOK BY ID */
router.get('/:id', function(req, res, next) {
  Book.findById(req.params.id, function (err, post) {
    if (err) return next(err);
    res.json(post);
  });
});

/* SAVE BOOK */
router.post('/', function(req, res, next) {
  Book.create(req.body, function (err, post) {
    if (err) return next(err);
    res.json(post);
  });
});

/* UPDATE BOOK */
router.put('/:id', function(req, res, next) {
  Book.findByIdAndUpdate(req.params.id, req.body, function (err, post) {
    if (err) return next(err);
    res.json(post);
  });
});

/* DELETE BOOK */
router.delete('/:id', function(req, res, next) {
  Book.findByIdAndRemove(req.params.id, req.body, function (err, post) {
    if (err) return next(err);
    res.json(post);
  });
});

module.exports = router;

Now, re-run our MEAN application. Open another terminal then type this command to test REST API.

curl -i -H "Accept: application/json" localhost:3000/book

If that command return response like below then REST API is ready to go.

HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 2
ETag: W/"2-11FxOYiYfpMxmANj4kGJzg"
Date: Sat, 18 Mar 2017 23:01:05 GMT
Connection: keep-alive

 


4. Create Angular 2 Providers or Services

To make REST API accessible from Angular 2 application first, create Angular 2 providers or services by typing this command.

ng g service book

That command will create files required by book service.

installing service
  create src/app/book.service.spec.ts
  create src/app/book.service.ts

Add book service manually to app.module.ts. Put import for book service.

import { BookService } from './book.service';

Add it to @NgModule providers.

providers: [BookService],

Next, open and edit src/app/book.service.ts. Replace all codes with this codes.

import { Injectable } from '@angular/core';
import { Http, Headers } from '@angular/http';
import 'rxjs/add/operator/map';

@Injectable()
export class BookService {

  constructor(private http: Http) { }

  getAllBooks() {
    return new Promise((resolve, reject) => {
      this.http.get('/book')
        .map(res => res.json())
        .subscribe(res => {
          resolve(res);
        }, (err) => {
          reject(err);
        });
    });
  }

  showBook(id) {
    return new Promise((resolve, reject) => {
        this.http.get('/book/' + id)
          .map(res => res.json())
          .subscribe(res => {
            resolve(res)
        }, (err) => {
          reject(err);
        });
    });
  }

  saveBook(data) {
    return new Promise((resolve, reject) => {
        this.http.post('/book', data)
          .map(res => res.json())
          .subscribe(res => {
            resolve(res);
          }, (err) => {
            reject(err);
          });
    });
  }

  updateBook(id, data) {
    return new Promise((resolve, reject) => {
        this.http.put('/book/'+id, data)
          .map(res => res.json())
          .subscribe(res => {
            resolve(res);
          }, (err) => {
            reject(err);
          });
    });
  }

  deleteBook(id) {
    return new Promise((resolve, reject) => {
        this.http.delete('/book/'+id)
          .subscribe(res => {
            resolve(res);
          }, (err) => {
            reject(err);
          });
    });
  }

}

That codes using Promise response instead of Observable.

 


5. Create Angular 2 Component for Book List

To create Angular 2 Component, simply run this command.

ng g component book

That command will generate all required files for build book component and also automatically added book component to app.module.ts.

installing component
  create src/app/book/book.component.css
  create src/app/book/book.component.html
  create src/app/book/book.component.spec.ts
  create src/app/book/book.component.ts
  update src/app/app.module.ts

Now, we will create book list in book component. Open and edit src/app/book.component.ts then replace all codes with this.

import { Component, OnInit } from '@angular/core';
import { BookService } from '../book.service';

@Component({
  selector: 'app-book',
  templateUrl: './book.component.html',
  styleUrls: ['./book.component.css']
})
export class BookComponent implements OnInit {

  books: any;

  constructor(private bookService: BookService) { }

  ngOnInit() {
    this.getBookList();
  }

  getBookList() {
    this.bookService.getAllBooks().then((res) => {
      this.books = res;
    }, (err) => {
      console.log(err);
    });
  }

}

Next, we will edit the view. But firstly, add bootstrap library to index.html.

<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <title>MeanApp</title>
  <base href="/">

  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" type="image/x-icon" href="favicon.ico">
  <!-- Latest compiled and minified CSS -->
  <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
  <!-- Optional theme -->
  <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap-theme.min.css" integrity="sha384-rHyoN1iRsVXV4nD0JutlnGaslCJuC7uwjduW9SVrLvRYooPp2bWYgmgJQIXwl/Sp" crossorigin="anonymous">
</head>
<body>
  <app-root>Loading...</app-root>
  <!-- Latest compiled and minified JavaScript -->
  <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
</body>
</html>

To use book component as default landing page, open and edit src/app/app.module.ts the add import for Routing.

import { RouterModule } from '@angular/router';

Create constant router for routing to book component.

const ROUTES = [
  { path: '', redirectTo: 'books', pathMatch: 'full' },
  { path: 'books', component: BookComponent }
];

In @NgModule imports section add ROUTES constant, so imports section will be like this.

imports: [
  BrowserModule,
  FormsModule,
  HttpModule,
  RouterModule.forRoot(ROUTES)
],

To prevent error 404 add this import.

import { HashLocationStrategy, LocationStrategy } from '@angular/common';

Then add this configuration codes in @NgModule providers section.

providers: [
  BookService,
  {provide: LocationStrategy, useClass: HashLocationStrategy}
],

Now, open and edit src/app/app.component.html then replace all codes with this.

<router-outlet></router-outlet>

Back to src/app/book/book.component.html then replace all codes with this.

<div class="container">
  <h1>Book Catalogue</h1>
  <table class="table">
    <thead>
      <tr>
        <th>Title</th>
        <th>Author</th>
        <th>Action</th>
      </tr>
    </thead>
    <tbody>
      <tr *ngFor="let book of books">
        <td>{{ book.title }}</td>
        <td>{{ book.author }}</td>
        <td>Show Detail</td>
      </tr>
    </tbody>
  </table>
</div>

Now, we have to test our MEAN app with only list page. Build then run the application.

ng build
nodemon

Reopen again 'localhost:3000' from your browser, it should be redirected to book list page like this.

Tutorial Building CRUD App from Scratch using MEAN Stack (Angular 2) - Angular Page run from nodemon

 


6. Create Angular 2 Component for Show Book Detail

Same as previous section, type this command to generate new component.

ng g component book-detail

Open and edit src/app/book-detail/book-detail.component.ts. Replace all codes with this.

import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { BookService } from '../book.service';

@Component({
  selector: 'app-book-detail',
  templateUrl: './book-detail.component.html',
  styleUrls: ['./book-detail.component.css']
})
export class BookDetailComponent implements OnInit {

  book = {};

  constructor(private route: ActivatedRoute, private bookService: BookService) { }

  ngOnInit() {
    this.getBookDetail(this.route.snapshot.params['id']);
  }

  getBookDetail(id) {
    this.bookService.showBook(id).then((res) => {
      this.book = res;
      console.log(this.book);
    }, (err) => {
      console.log(err);
    });
  }

}

Open and edit src/app/book-detail/book-detail.component.html. Replace all codes with this.

<div class="container">
  <h1>{{ book.title }}</h1>
  <dl class="list">
    <dt>ISBN</dt>
    <dd>{{ book.isbn }}</dd>
    <dt>Author</dt>
    <dd>{{ book.author }}</dd>
    <dt>Publisher</dt>
    <dd>{{ book.publisher }}</dd>
    <dt>Price</dt>
    <dd>{{ book.price }}</dd>
    <dt>Update Date</dt>
    <dd>{{ book.updated_at }}</dd>
  </dl>
</div>

 


7. Create Angular 2 Component for Add New Book

To create a component for add new Book, type this command as usually.

ng g component book-create

Add router to src/app/app.module.ts.

const appRoutes: Routes = [
  { path: '', redirectTo: 'books', pathMatch: 'full' },
  { path: 'books', component: BookComponent },
  { path: 'book-details/:id', component: BookDetailComponent },
  { path: 'book-create', component: BookCreateComponent }
];

Add 'book-create' link on book.component.html.

<h1>Book Catalogue
  <a [routerLink]="['/book-create']" class="btn btn-default btn-lg">
    <span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
  </a>
</h1>

Now, open and edit src/app/book/book-create.component.ts then replace all with this codes.

import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { BookService } from '../book.service';

@Component({
  selector: 'app-book-create',
  templateUrl: './book-create.component.html',
  styleUrls: ['./book-create.component.css']
})
export class BookCreateComponent implements OnInit {

  book = {};

  constructor(private bookService: BookService, private router: Router) { }

  ngOnInit() {
  }

  saveBook() {
    this.bookService.saveBook(this.book).then((result) => {
      let id = result['_id'];
      this.router.navigate(['/book-details', id]);
    }, (err) => {
      console.log(err);
    });
  }

}

Modify src/app/book-create/book-create.component.html, replace all with this HTML tags.

<div class="container">
  <h1>Add New Book</h1>
  <div class="row">
    <div class="col-md-6">
      <form (ngSubmit)="saveBook()" #bookForm="ngForm">
        <div class="form-group">
          <label for="name">ISBN</label>
          <input type="text" class="form-control" [(ngModel)]="book.isbn" name="isbn" required>
        </div>
        <div class="form-group">
          <label for="name">Title</label>
          <input type="text" class="form-control" [(ngModel)]="book.title" name="title" required>
        </div>
        <div class="form-group">
          <label for="name">Author</label>
          <input type="text" class="form-control" [(ngModel)]="book.author" name="author" required>
        </div>
        <div class="form-group">
          <label for="name">Publisher</label>
          <input type="text" class="form-control" [(ngModel)]="book.publisher" name="publisher" required>
        </div>
        <div class="form-group">
          <label for="name">Price</label>
          <input type="number" class="form-control" [(ngModel)]="book.price" name="price" required>
        </div>
        <div class="form-group">
          <button type="submit" class="btn btn-success" [disabled]="!bookForm.form.valid">Save</button>
        </div>
      </form>
    </div>
  </div>
</div>

 


8. Create Angular 2 Component for Edit Book

As usually, we will generate component for edit book. Type this command for doing that.

ng g component book-edit

Add route in src/app/app.module.ts so, it looks like this.

const appRoutes: Routes = [
  { path: '', redirectTo: 'books', pathMatch: 'full' },
  { path: 'books', component: BookComponent },
  { path: 'book-details/:id', component: BookDetailComponent },
  { path: 'book-create', component: BookCreateComponent },
  { path: 'book-edit/:id', component: BookEditComponent }
];

Open and edit again src/app/book-details/book-details.component.html and add edit routeLink in the last line.

<div class="row">
  <div class="col-md-12">
    <a [routerLink]="['/book-edit', book._id]" class="btn btn-success">EDIT</a>
  </div>
</div>

Now, open and edit src/app/book-edit/book-edit.component.ts then replace all codes with this.

import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { BookService } from '../book.service';

@Component({
  selector: 'app-book-edit',
  templateUrl: './book-edit.component.html',
  styleUrls: ['./book-edit.component.css']
})
export class BookEditComponent implements OnInit {

  book = {};

  constructor(private bookService: BookService, private router: Router, private route: ActivatedRoute) { }

  ngOnInit() {
    this.getBook(this.route.snapshot.params['id']);
  }

  getBook(id) {
    this.bookService.showBook(id).then((res) => {
      this.book = res;
      console.log(this.book);
    }, (err) => {
      console.log(err);
    });
  }

  updateBook(id) {
    this.bookService.updateBook(id, this.book).then((result) => {
      let id = result['_id'];
      this.router.navigate(['/book-details', id]);
    }, (err) => {
      console.log(err);
    });
  }

}

Open and edit src/app/book-edit/book-edit.component.html then replace all codes with this.

<div class="container">
  <h1>Edit Book</h1>
  <div class="row">
    <div class="col-md-6">
      <form (ngSubmit)="updateBook(book._id)" #bookForm="ngForm">
        <div class="form-group">
          <label for="name">ISBN</label>
          <input type="text" class="form-control" [(ngModel)]="book.isbn" name="isbn" required>
        </div>
        <div class="form-group">
          <label for="name">Title</label>
          <input type="text" class="form-control" [(ngModel)]="book.title" name="title" required>
        </div>
        <div class="form-group">
          <label for="name">Author</label>
          <input type="text" class="form-control" [(ngModel)]="book.author" name="author" required>
        </div>
        <div class="form-group">
          <label for="name">Publisher</label>
          <input type="text" class="form-control" [(ngModel)]="book.publisher" name="publisher" required>
        </div>
        <div class="form-group">
          <label for="name">Price</label>
          <input type="number" class="form-control" [(ngModel)]="book.price" name="price" required>
        </div>
        <div class="form-group">
          <button type="submit" class="btn btn-success" [disabled]="!bookForm.form.valid">Update</button>
        </div>
      </form>
    </div>
  </div>
</div>


9. Create Delete Function on Book-Detail Component

The last thing in this MEAN app tutorial is to create delete function that put on the book detail component. Open and edit src/app/book-detail/book-detail.component.ts then add this function.

deleteBook(id) {
  this.bookService.deleteBook(id).then((result) => {
    this.router.navigate(['/books']);
  }, (err) => {
    console.log(err);
  });
}

Don't forget to add import for Router and inject Router in constructor.

import { ActivatedRoute, Router } from '@angular/router';
constructor(private route: ActivatedRoute, private router: Router, private bookService: BookService) { }

Add delete button in src/app/book-detail/book-detail.component.html on the right of Edit routerLink.

<div class="row">
  <div class="col-md-12">
    <a [routerLink]="['/book-edit', book._id]" class="btn btn-success">EDIT</a>
    <button class="btn btn-danger" type="button" (click)="deleteBook(book._id)">DELETE</button>
  </div>
</div>

This is it, now we have completed build CRUD web application using MEAN stack with Angular 2. Rebuild the application then run again to test the fully functionalities.

ng build
nodemon

Please feel free to give suggestion or anything to improve this tutorial.

The full source code can find on Our GitHub.

Thanks.