The MEAN stack allows you to build complete applications using one programming language: JavaScript. In this tutorial, we made upon the first part (Creating an Angular app) which built the front-end, and this part builds the backend with a RESTful API and Database.

REST API with Node.js

We are going to use express-generator and create a folder called server.

First, install the generator packages:

1
npm i -g express-generator

Note: You should have Node and NPM/Yarn installed.

REST API using ExpressJS

Now let’s scaffold the App using the generator:

1
express server -e

Let’s install all its dependencies on the server folder:

1
cd server && npm i

and now let’s make sure it’s working:

1
npm start

Go to localhost on port 3000 and make sure you can see a “Welcome to Express.”

http://localhost:3000/

Changes: a3fcacd - REST API using ExpressJS: scaffold

Creating a host alias for the server

We want to run the server to work regardless of the environment where we run it. (It will be useful for Docker later on)

For that we can create an alias by editing the hosts:

  • Windows: c:\windows\system32\drivers\etc\hosts
  • Linux/Mac: /etc/hosts

Once you can open the file, you drop the following line at the end:

1
127.0.0.1 server

Now you should be able to access the server by visiting:

http://server:3000/

(If you have trouble editing the host file, take a look here)

Creating API routes and responding to requests

Now we are going to create a new route:

1
/api/todos[/:id]

This route will get all our todos, update, and delete them.

Create a new router file called todos in the following path:

1
touch server/routes/todos.js

and add this initial content:

server/routes/todos.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const express = require('express');
const router = express.Router();

const TODOS = [
{ title: 'Create API to get this list', isDone: true },
{ title: 'Connect API with Angular', isDone: true },
{ title: 'Connect server with mongo', isDone: false },
{ title: 'Publish app', isDone: false },
];

/* GET /api/todos */
router.get('/', async (req, res) => {
try {
res.json(TODOS);
} catch (error) {
res.status(500).json({ error });
}
});

module.exports = router;

All this is doing is replying to the GET commands and returning a hard-coded list of todos. We will replace it later to get data from mongo instead.

Then we need to register the route as follows:

server/app.js
1
2
3
4
5
var todosRouter = require('./routes/todos');

// ...

app.use('/api/todos', todosRouter);

The method use registers the new path /api/todos. When we get any call on this path, our todosRouter will handle it.

You can restart your server or use nodemon to pick up changes and refresh the browser.

1
2
## npm i -g nodemon
nodemon server/bin/www

That should get your server running. Now you can see it in action using cURL:

1
2
curl -XGET server:3000/api/todos
## curl -XGET localhost:3000/api/todos

This command should get you all the lists in JSON format!

In the next step, we will query the server using Angular instead of curl. After that, we will complete the rest of the operations (update, delete, create).

6f8a502 - Creating API routes and responding to requests

Connecting REST API with Angular App.

Let’s now prepare our angular App to use the server API that we just created.

As you might know, when you run ng serve, it will trigger a development server. However, our API is an entirely different server. To be able to connect the two, we need to create a proxy.

Creating a proxy in Angular to talk to the API server

Let’s create a new file that will tell Angular when to look for specific HTTP paths. In this case, we are going to defer all /api to our express server.

src/proxy.conf.json
1
2
3
4
5
6
{
"/api": {
"target": "http://server:3000",
"secure": false
}
}

(This will need the host alias from the step before)

Then, we have to tell Angular to load this file when we are serving the App. We are going to do that in the angular.json file.

If you are using the same version of angular CLI, you need to insert this on line 71:

1
"proxyConfig": "src/proxy.conf.json"

For some context, here are the surrounding elements:

angular.json > projects > Todos > architect > serve > options
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
// ...
"projects": {
"Todos": {
// ...
"architect": {
// ...
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"options": {
"browserTarget": "Todos:build",
"proxyConfig": "src/proxy.conf.json" // <-- Insert this line
},
// ...
},
// ...
}
}},
"defaultProject": "Todos"
}

Now our App will pass all requests that start with /api to http://localhost:3000 (or whatever path you specified on the proxy.conf).

Next, we are going to make use of these new routes!

e81ddb8 - Creating a proxy in Angular to talk to the API server

Using HTTP Client to talk to the server

To talk to the server, we are going to use the HttpClient module.

Let’s go to the app.module and let’s import it:

src/app/app.module.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { HttpClientModule } from '@angular/common/http';

// ...

@NgModule({
declarations: [
AppComponent,
TodoComponent
],
imports: [
BrowserModule,
AppRoutingModule,
FormsModule,
RouterModule.forRoot(routes),
HttpClientModule, // <---- import module
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }

Now that the HttpClient is available in our App let’s add it to the service and make use of it.

src/app/todo/todo.service.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { HttpClient } from '@angular/common/http';

//...

const API = '/api/todos';

//...

@Injectable({
providedIn: 'root'
})
export class TodoService {

constructor(private http: HttpClient) { }

get(params = {}) {
return this.http.get(API, { params });
}

// ...

We change the TodoService.get to use HTTP client. However, the component was responding to a Promise, and the HTTP.get returns an Observable. So, let’s change it.

Change the getTodos method from the old one to use this one that handles an observable.

src/app/todo/todo.component.ts
1
2
3
4
5
6
getTodos(query = '') {
return this.todoService.get(query).subscribe(todos => {
this.todos = todos;
this.activeTasks = this.todos.filter(todo => !todo.isDone).length;
});
}

The main difference is that instead of a .then, we are using .subscribe. Everything else remains the same (for now).

That’s it, let’s test it out!

Run these commands on your terminal:

1
2
## run node server
nodemon server/bin/www

on another terminal session run also:

1
2
## run angular App
ng serve

Once you have both running, you can go to http://localhost:4200/all, and you can verify that it’s coming from your server!

If you are running nodemon, you can change the TODOS on server/routes/todos.js and refresh the browser, and see how it changes.

But, we don’t want to have hard-coded tasks. Let’s create a proper DB with Mongo.

Setting up MongoDB

It’s time to get MongoDB up and running. If don’t have it installed, you have a couple of options:

Docker (Windows/macOS/Linux) [Preferred]
  1. Download the docker engine

  2. Pull Mongo image

    1
    docker pull mongo

    NOTE: More details in the rest of the post.

Official Website (Windows/macOS/Linux)

You can download it from here:

https://docs.mongodb.com/manual/administration/install-community/

Brew (macOS)
1
2
brew tap mongodb/brew
brew install mongodb-community

We are going to use Docker since it’s an excellent way to have everything running together with one command. Also, you can deploy it to the cloud and scale it quickly.

Dockerizing the MEAN stack

Let’s get everything running (Node Server, Angular, and Mongo). We will create a docker-compose file, which is going to list all our services, and we can run them all at once.

docker-compose.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
version: "3.7"

services:
app:
image: node:lts-alpine
working_dir: /app
volumes:
- ./:/app
ports:
- 4200:4200
# --host 0.0.0.0 to listen to all the interfaces from the container (dev env)
command: >
sh -c "npm install &&
npx ng serve --host 0.0.0.0"

server:
image: node:lts-alpine
working_dir: /server
volumes:
- ./server:/server
# port 3000 has to match src/proxy.conf.json
ports:
- 3000:3000
depends_on:
- mongo
environment:
MONGO_HOST: mongo
command: >
sh -c "npm i -g nodemon && npm install && nodemon ./bin/www"

mongo:
image: mongo

All right, now we can get the whole full-stack App running with one command:

1
docker-compose up --build

NOTE: close other terminals running a web server so the ports don’t conflict. The docker-compose command will create 3 containers for our Angular App, Node Server, and Mongo DB. You can also see all the logs in one place.

After you wait a minute or so, you should be able to open the App on http://localhost:4200/.

Now we can make use of mongo. Keep docker-compose running and now let’s remove the hard-coded tests and use the database.

0763db0 - docker compose

Creating MongoDB schema with Mongoose

Let’s install Mongoose, which is a library for managing MongoDB from Node.js.

1
cd server && npm i mongoose@5.9.18

NOTE: make sure you installed it on the ./server/package.json, rather than the client-side packages ./packages.json.

The first thing we need to do is to connect to Mongo when our server starts. Go to server/app.js and add the following code:

server/app.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const mongoose = require('mongoose');

// connect to db
const user = process.env.MONGO_USER;
const mongoPort = process.env.MONGO_PORT || 27017;
const mongoHost = process.env.MONGO_HOST || 'localhost';
const auth = user ? `${user}:${process.env.MONGO_PASS}@` : '';
const DB_STRING = `mongodb://${auth}${mongoHost}:${mongoPort}/todos`;

console.log(`Running node ${process.version}...`);
console.log(`Connecting to DB... ${DB_STRING}`);

const config = { useNewUrlParser: true, useUnifiedTopology: true};
mongoose.connect(DB_STRING, config)
.then(() => console.log(`Connected!`))
.catch(console.error);

15f6e25 - add db string to connect to mongo

We can pass some ENV variables like MONGO_HOST. If we run it locally, it will use localhost, but if you run it on Docker, we want to pass a hostname. You can see that in the docker-compose.yml file.

Now, let’s define our data model for Mongo. Let’s create a folder models inside server and add a file called “todos.js”.

server/models/todo.js
1
2
3
4
5
6
7
8
9
10
11
12
const mongoose = require('mongoose');

const { Schema, model } = mongoose;

const TodoSchema = new Schema({
title: String,
isDone: Boolean,
notes: String,
updated_at: { type: Date, default: Date.now },
});

module.exports = model('Todo', TodoSchema);

The new schema is defining what fields we want to store and what the types are. The updated_at will update automatically when we create a new todo.

436b0ad - npm i mongoose

b2674f3 - Creating MongoDB schema with Mongoose

Adding all the API routes to modify data in the DB

Let’s add all the routes to create, read, update, and delete data from Mongo.

The Mongoose library provides some convenient methods to do CRUD operations:

  • ** Todo.find**: find data matching a given query. ({}, get all, while {isDone: true} get only completed tasks).
  • ** Todo.create**: Create a new todo
  • ** Todo.findByIdAndUpdate**: Find Todo by given id and update its content.
  • ** Todo.findByIdAndDelete**: Find Todo by given id and delete it.
  • ** Todo.deleteMany**: Delete everything matching a given query.

Here are the routes by their matching HTTP verb (GET, PUT, POST, DELETE). In the next sections, we will test all these routes and go over some more details.

server/routes/todos.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
const express = require('express');
const router = express.Router();

const Todo = require('../models/todo');

/* GET /api/todos */
router.get('/', async (req, res) => {
try {
const list = await Todo.find(req.query);
res.json(list);
} catch (error) {
res.status(500).json({ error });
}
});

/* POST /api/todos */
router.post('/', async (req, res) => {
try {
const todo = await Todo.create(req.body);
res.json(todo);
} catch (error) {
res.status(500).json({ error });
}
});

/* PUT /api/todos/:id */
router.put('/:id', async (req, res) => {
try {
const options = { new: true };
const todo = await Todo.findByIdAndUpdate(req.params.id, req.body, options);
res.json(todo);
} catch (error) {
res.status(500).json({ error });
}
});

/* DELETE /api/todos/:id */
router.delete('/:id', async (req, res) => {
try {
const todo = await Todo.findByIdAndDelete(req.params.id);
res.json(todo);
} catch (error) {
res.status(500).json({ error });
}
});

// Beaware: it can delete all data from db if body is empty (DON'T expoose deleteMany in PRODUCTION!)
/* DELETE /api/todos */
router.delete('/', async (req, res) => {
try {
const todo = await Todo.deleteMany(req.body);
res.json(todo);
} catch (error) {
res.status(500).json({ error });
}
});

module.exports = router;

We added many routes to this step. Take your time to go through them. In the next section, we will test them using curl and then integrated them with Angular.

f4f2281 - Adding all all the API routes to modify data in DB

Testing the API CRUD operations

Since we installed a new package, mongoose, we have to run npm install in the docker containers. Otherwise, file changes are picked up automatically, and you don’t need to restart.

Stop docker-compose and start it again docker-compose up --build.

Creating a new task and getting lists

You can create a new task using the following command:

1
curl -XPOST server:3000/api/todos -H "Content-Type: application/json" -d '{"title": "CRUD API", "isDone": false}'

Now, let’s see if it’s there:

1
curl -XGET server:3000/api/todos

You should have got something like this:

1
[{"_id":"5edc2a6d0c41d60054ad715f","title":"CRUD API","isDone":false,"updated_at":"2020-06-06T23:44:45.966Z","__v":0}]⏎

You can also check Angular on http://localhost:4200/all. The new task should be there!

Update data with PUT method

If you remember from your routes file, we are using the method PUT to update tasks.

server/routes/todos.js
1
2
3
4
5
6
7
8
9
10
/* PUT /api/todos */
router.put('/:id', async (req, res) => {
try {
const options = { new: true };
const todo = await Todo.findByIdAndUpdate(req.params.id, req.body, options);
res.json(todo);
} catch (error) {
res.json(500, { error });
}
});

By default findByIdAndUpdate returns the original document. We are passing { new: true } so we can return the updated document.

For updating a task you need the _id. You can get it from the previous step, when we listed all the tasks. For my case the _id is 5edc2a6d0c41d60054ad715f, find yours and replace it in the next command:

1
curl -XPUT server:3000/api/todos/5edc2a6d0c41d60054ad715f -H "Content-Type: application/json" -d '{"title": "Finish PUT API", "isDone": true, "note": "New Field"}'

As you can see in the last update, we can modify existing fields and add new values like the note field.

Erasing data with the DELETE method

For our todo route, we also defined the DELETE method. Similar to the update, we need to pass and id.

server/routes/todos.js
1
2
3
4
5
6
7
8
9
/* DELETE /api/todos */
router.delete('/:id', async (req, res) => {
try {
const todo = await Todo.findByIdAndDelete(req.params.id);
res.json(todo);
} catch (error) {
res.status(500).json({ error });
}
});

Again, remember to replace the next call with yout _id:

1
curl -X DELETE server:3000/api/todos/5edc2a6d0c41d60054ad715f

If you check the UI, all tasks will be gone: http://localhost:4200/all.

As much fun as curl is, let’s move on and complete all these functionalities in Angular.

Angular Service to talk to the server

There are two main changes that we need to make in other to use the API server.

  1. We need to change the TodoService service to use HTTP client.
  2. Change the TodoComponent component to use the methods.

Angular service using the HTTP client

In the following code, we are using the HTTP client, to make the appropriate calls:

src/app/todo/todo.service.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';

export interface ITodo {
_id?: string;
title: string;
isDone: boolean;
notes: string;
update_at: string;
editing ?: boolean;
}

const API = '/api/todos';

@Injectable({
providedIn: 'root'
})
export class TodoService {
constructor(private http: HttpClient) { }

get(params = {}) {
return this.http.get(API, { params });
}

add(data: ITodo) {
return this.http.post(API, data);
}

put(changed: ITodo) {
return this.http.put(`${API}/${changed._id}`, changed);
}

toggle(selected: ITodo) {
selected.isDone = !selected.isDone;
return this.put(selected);
}

delete(selected: ITodo) {
return this.http.delete(`${API}/${selected._id}`);
}

deleteCompleted(body = { isDone: true }) {
return this.http.request('delete', `${API}`, { body });
}
}

The Todo service matches the HTTP verbs that we used in curl and passes the payloads.

Let’s now change the TodoComponent that goes along with these changes.

a93291c - Angular service using HTTP client

Angular TodoComponet updates

Here’s what your component should look like this:

src/app/todo/todo.component.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';

import { TodoService, ITodo } from './todo.service';

@Component({
selector: 'app-todo',
templateUrl: './todo.component.html',
styleUrls: ['./todo.component.scss'],
providers: [TodoService]
})
export class TodoComponent implements OnInit {
public todos: ITodo[];
public activeTasks: number;
public newTodo: string;
public path: string;
public mapToQuery = {
all: {},
active: { isDone: false },
completed: { isDone: true },
};
constructor(private todoService: TodoService, private route: ActivatedRoute) { }

ngOnInit() {
this.route.params.subscribe(params => {
this.path = params.status;
this.getTodos(this.path);
});
}

addTodo() {
this.todoService
.add({ title: this.newTodo, isDone: false } as unknown as ITodo)
.subscribe(() => {
this.getTodos();
this.newTodo = ''; // clear input form value
});
}

getTodos(route = 'all') {
const query = this.mapToQuery[route];
return this.todoService
.get(query)
.subscribe((todos: ITodo[]) => {
this.todos = todos;
this.activeTasks = this.todos.filter(todo => !todo.isDone).length;
});
}

updateTodo(todo: ITodo, newValue: string) {
todo.title = newValue;
return this.todoService.put(todo).subscribe(() => this.getTodos());
}

destroyTodo(todo: ITodo) {
this.todoService.delete(todo).subscribe(() => this.getTodos());
}

toggleTodo(todo: ITodo) {
this.todoService.toggle(todo).subscribe(() => this.getTodos());
}

clearCompleted() {
this.todoService.deleteCompleted().subscribe(() => this.getTodos());
}
}

Let’s go over each part in the next sections.

Sending Queries with HTTP GET

In the component, one the first thing we do is check the route params (path):

recap: src/app/todo/todo.component.ts (exerpt)
1
2
3
4
5
6
ngOnInit() {
this.route.params.subscribe(params => {
this.path = params['status'];
this.getTodos(this.path);
});
}

When you click on the buttons All, Active, and Completed, that will trigger a route change.

To recap, these buttons use the router link. So, every time you click on them, they will change the URL.

recap: src/app/todo/todo.component.html (exerpt)
1
2
3
4
5
6
7
8
9
10
11
<ul class="filters">
<li>
<a [routerLink]="['/all']" [class.selected]="path === 'all'">All</a>
</li>
<li>
<a [routerLink]="['/active']" [class.selected]="path === 'active'">Active</a>
</li>
<li>
<a [routerLink]="['/completed']" [class.selected]="path === 'completed'">Completed</a>
</li>
</ul>

After we change the URL, the next thing we do is to call getTodos. Let’s see that next.

Get all todos

We can get all services using the following:

src/app/todo/todo.component.ts (exerpt)
1
2
3
4
5
6
7
8
9
getTodos(route = 'all') {
const query = this.mapToQuery[route];
return this.todoService
.get(query)
.subscribe((todos: ITodo[]) => {
this.todos = todos;
this.activeTasks = this.todos.filter(todo => !todo.isDone).length;
});
}

We this.todoService.get will issue an HTTP get and retrieve all the tasks from the database and update the todos. It also updates the number of active tasks (the ones that are not done).

The getTodos receives an argument (route) with the path that will be one of these: all, active, or complete. However, MongoDB doesn’t understand these words. We have to map it (mapToQuery) to something a proper query like { isDone: true }. This MongoDB will understand.

e33d540 - Angular TodoComponet updates

Modifying the todos

All the other operations, like the update, clear, toggle, are very similar. They trigger an action and then call getTodos, so the UI is up to date with the latest changes.

That’s all!