It’s been quite some time since the release of Angular 4. It has come up with lots of new features and enhancements. If you wanted to try out Angular 4 and learn how to use it with Spring Boot, then this blog post is for you.

In this post, We’ll build a fully-fledged todo app with Spring Boot & MongoDB in the backend and Angular 4 in the Frontend.

Following is a screenshot of the todo app that we’re going to build in this tutorial.

If this looks cool to you, then stay with me till the end. I’ll show you how to build the app from scratch.

Creating Spring Boot backend

1. Bootstrapping the Project

We’ll use Spring Initializer web app to generate our spring boot project.

  1. Head over to http://start.spring.io/.
  2. Enter Artifact’s value as todoapp.
  3. Add Web and MongoDB in the dependencies section.
  4. Click Generate Project to generate and download the project.

Spring Boot MongoDB Angular 4 Project

Once the project is downloaded, unzip it and import it into your favorite IDE. The Project’s directory structure should look like this -

Spring Boot Backend Directory Structure

2. Configuring MongoDB database

Spring Boot tries to auto-configure most of the stuff for you based on the dependencies that you have added in the pom.xml file.

Since we have added spring-boot-starter-mongodb dependency, Spring Boot tries to build a connection with MongoDB by reading the database configuration from application.properties file.

Open application.properties file and add the following mongodb properties -

# MONGODB (MongoProperties)
spring.data.mongodb.uri=mongodb://localhost:27017/todoapp

Note that, for the above configuration to work, you need to have MongoDB installed in your system.

Checkout the official mongodb doc for installing MongoDB in your System.

If you don’t want to install MongoDB locally, you can use one of the free mongodb database-as-a-service providers like MongoLab.

MongoLab gives you 500 MB worth of data in their free plan. You can create a database with their free plan. After creating a database, you’ll get a mongodb connection uri of the following form -

mongodb://<dbuser>:<dbpassword>@ds161169.mlab.com:61169/testdatabase 

Just add the connection uri to the application.properties file and you’re ready to go.

3. Creating the Todo model

Let’s now create the Todo model which will be mapped to a Document in the mongodb database. Create a new package models inside com.example.todoapp, and add a file Todo.java inside models package with the following contents -

package com.example.todoapp.models;

import java.util.Date;
import javax.validation.constraints.Size;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import org.hibernate.validator.constraints.NotBlank;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.index.Indexed;
import org.springframework.data.mongodb.core.mapping.Document;

@Document(collection="todos")
@JsonIgnoreProperties(value = {"createdAt"}, allowGetters = true)
public class Todo {
    @Id
    private String id;
    
    @NotBlank
    @Size(max=100)
    @Indexed(unique=true)
    private String title;
    
    private Boolean completed = false;
    
    private Date createdAt = new Date();
    
    public Todo() {
        super();
    }
    
    public Todo(String title) {
        this.title = title;
    }
    
    public String getId() {
        return id;
    }
    
    public void setId(String id) {
        this.id = id;
    }
    
    public String getTitle() {
        return title;
    }
    
    public void setTitle(String title) {
        this.title = title;
    }
    
    public Boolean getCompleted() {
        return completed;
    }
    
    public void setCompleted(Boolean completed) {
        this.completed = completed;
    }
    
    public Date getCreatedAt() {
        return createdAt;
    }
    
    public void setCreatedAt(Date createdAt) {
        this.createdAt = createdAt;
    }
    
    @Override
    public String toString() {
        return String.format(
                "Todo[id=%s, title='%s', completed='%s']",
                id, title, completed);
    }
}

We have annotated title with @Indexed annotation and marked it as unique. This creates a unique index on title field.

Also, We make sure that the todo’s title is not blank by annotating it with @NotBlank annotation.

The @JsonIgnoreProperties annotation is used to ignore createdAt field during deserialization. We don’t want clients to send the createdAt value. If they send a value for this field, we’ll simply ignore it.

4. Creating TodoRepository for accessing the database

Next, we need to create TodoRepository for accessing data from the database.

First, create a new package repositories inside com.example.todoapp.

Then, create an interface named TodoRepository inside repositories package with the following contents -

package com.example.todoapp.repositories;

import com.example.todoapp.models.Todo;
import org.springframework.data.mongodb.repository.MongoRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface TodoRepository extends MongoRepository<Todo, String> {

}

We’ve extended TodoRepository with MongoRepository interface provided by spring-data-mongodb. The MongoRepository interface defines methods for all the CRUD operations on the Document like finAll(), fineOne(), save(), delete() etc.

Spring Boot automatically plugs in an implementation of MongoRepository interface called SimpleMongoRepository at runtime. So, All of the CRUD methods defined by MongoRepository are readily available to you without doing anything.

You can check out all the methods available for use from SimpleMongoRepository’s documentation.

5. Creating the APIs - TodoController

Finally, let’s create the APIs which will be exposed to the clients. Create a new package controllers inside com.example.todoapp and add a file TodoController.java inside controllers package with the following code -

package com.example.todoapp.controllers;

import javax.validation.Valid;
import com.example.todoapp.models.Todo;
import com.example.todoapp.repositories.TodoRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Sort;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;

@RestController
@RequestMapping("/api")
@CrossOrigin("*")
public class TodoController {

    @Autowired
    TodoRepository todoRepository;

    @GetMapping("/todos")
    public List<Todo> getAllTodos() {
        Sort sortByCreatedAtDesc = new Sort(Sort.Direction.DESC, "createdAt");
        return todoRepository.findAll(sortByCreatedAtDesc);
    }

    @PostMapping("/todos")
    public Todo createTodo(@Valid @RequestBody Todo todo) {
        todo.setCompleted(false);
        return todoRepository.save(todo);
    }

    @GetMapping(value="/todos/{id}")
    public ResponseEntity<Todo> getTodoById(@PathVariable("id") String id) {
        Todo todo = todoRepository.findOne(id);
        if(todo == null) {
            return new ResponseEntity<>(HttpStatus.NOT_FOUND);
        } else {
            return new ResponseEntity<>(todo, HttpStatus.OK);
        }
    }

    @PutMapping(value="/todos/{id}")
    public ResponseEntity<Todo> updateTodo(@PathVariable("id") String id,
                                           @Valid @RequestBody Todo todo) {
        Todo todoData = todoRepository.findOne(id);
        if(todoData == null) {
            return new ResponseEntity<>(HttpStatus.NOT_FOUND);
        }
        todoData.setTitle(todo.getTitle());
        todoData.setCompleted(todo.getCompleted());
        Todo updatedTodo = todoRepository.save(todoData);
        return new ResponseEntity<>(updatedTodo, HttpStatus.OK);
    }

    @DeleteMapping(value="/todos/{id}")
    public void deleteTodo(@PathVariable("id") String id) {
        todoRepository.delete(id);
    }
}

The @CrossOrigin annotation in the above controller is used to enable Cross-Origin requests. This is required because we’ll be accessing the apis from angular’s frontend server.

All right! Our backend work is complete now. You can run the app using -

mvn spring-boot:run

The app will start on port 8080. You can test the backend apis using postman or any other rest client of your choice.

Let’s now work on the Angular 4 frontend.

Creating Angular 4 Frontend

1. Generating the app using angular-cli

Angular-cli is a command line tool for creating production ready angular applications. It helps in creating, testing, bundling and deploying an angular project.

You can install angular by typing the following command in your terminal -

$ npm install -g @angular/cli

Once installed, you can generate a new project using ng new command -

$ ng new angular4-frontend

2. Running the app

Let’s first run the app generated by angular-cli and then we’ll change and add necessary files required for our application. For running the app, go to the app’s root directory and type ng serve.

$ cd angular4-frontend
$ ng serve --open

The --open option is used to automatically open the app in your default browser. The app will load in your default browser at http://localhost:4200 and display a welcome message -

Angular CLI Default App

3. Change template for AppComponent (app.component.html)

The template for the default welcome page that you see is defined in app.component.html. Remove everything in this file and add the following code -

<main>
  <todo-list></todo-list>
</main>

The <todo-list> tag will be used to display the TodoList component in this page. We’ll define the TodoListComponent shortly.

4. Create Todo class todo.ts

Before defining the TodoListComponent, let’s define a Todo class for working with Todos. create a new file todo.ts inside src/app folder and add the following code to it -

export class Todo {
  id: string;
  title: string;
  completed: boolean;
  createdAt: Date;
}

5. Create TodoListComponent todo-list.component.ts

We’ll now define the TodoListComponent. It will be used to display a list of todos, create a new todo, edit and update a todo.

Create a new file todo-list.component.ts inside src/app directory and add the following code to it -

import { Component, OnInit } from '@angular/core';
import { Todo } from './todo';
import { NgForm } from '@angular/forms';

@Component({
  selector: 'todo-list',
  templateUrl: './todo-list.component.html'
})

export class TodoListComponent implements OnInit {
  todos: Todo[];
  newTodo: Todo = new Todo();
  editing: boolean = false;
  editingTodo: Todo = new Todo();

  ngOnInit(): void {
    this.getTodos();
  }

  getTodos(): void {

  }

  createTodo(todoForm: NgForm): void {

  }

  deleteTodo(id: string): void {

  }

  updateTodo(todoData: Todo): void {

  }

  toggleCompleted(todoData: Todo): void {

  }

  editTodo(todoData: Todo): void {

  }

  clearEditing(): void {

  }
}

The selector for TodoListComponent is todo-list. Remember, We used <todo-list> tag in app.component.html to refer to TodoListComponent.

We’ll implement all the methods declared in the component in the next section. But, before that, we need to declare the TodoListComponent inside app.module.ts.

Just import TodoListComponent and add it to declarations array inside @NgModule -

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { FormsModule }   from '@angular/forms';
import { HttpModule }    from '@angular/http';

import { AppComponent } from './app.component';

import { TodoListComponent } from './todo-list.component';

@NgModule({
  declarations: [
    AppComponent,
    TodoListComponent
  ],
  imports: [
    BrowserModule,
    FormsModule,
    HttpModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

Note that, I have also added FormsModule and HttpModule inside app.module.ts because we’ll need them for handling form-binding and calling the rest APIs respectively.

6. Create template for TodoListComponent todo-list.component.html

Next, we’re gonna define the template for TodoListComponent. Create a new file todo-list.component.html inside src/app directory and add the following code to it -

<div class="todo-content">
    <h1 class="page-title">My Todos</h1>
    <div class="todo-create">
      <form #todoForm="ngForm" (ngSubmit) = "createTodo(todoForm)" novalidate>
        <input type="text" id="title" class="form-control" placeholder="Type a todo and press enter..." 
               required
               name="title" [(ngModel)]="newTodo.title"
               #title="ngModel" >
        <div *ngIf="title.errors && title.dirty"
             class="alert alert-danger">
            <div [hidden]="!title.errors.required">
              Title is required.
            </div>
        </div>
      </form>
    </div>
    <ul class="todo-list">
      <li *ngFor="let todo of todos" [class.completed]= "todo.completed === true" >
        <div class="todo-row" *ngIf="!editing || editingTodo.id != todo.id">
            <a class="todo-completed" (click)="toggleCompleted(todo)">
              <i class="material-icons toggle-completed-checkbox"></i> 
            </a> 
            <span class="todo-title">
              {{todo.title}}
            </span>
            <span class="todo-actions">
                <a (click)="editTodo(todo)">
                  <i class="material-icons edit">edit</i>
                </a>
                <a (click)="deleteTodo(todo.id)">
                  <i class="material-icons delete">clear</i>
                </a>
            </span>
        </div>
        <div class="todo-edit" *ngIf="editing && editingTodo.id === todo.id">
            <input class="form-control" type="text" 
             [(ngModel)]="editingTodo.title" required>
            <span class="edit-actions">
                <a (click)="updateTodo(editingTodo)">
                  <i class="material-icons">done</i>
                </a>
                <a (click)="clearEditing()">
                  <i class="material-icons">clear</i>
                </a>
            </span>
        </div>
      </li>
    </ul>
    <div class="no-todos" *ngIf="todos && todos.length == 0">
        <p>No Todos Found!</p>
    </div>
</div>

The template contains code to display a list of todos and call methods for editing a todo, deleting a todo, and marking a todo as complete.

7. Add Styles styles.css

Let’s add some styles to make our template look good. Note that, I’m adding all the styles in the global styles.css file. But, you can define styles related to a component in a separate css file and reference that css by defining styleUrls in the @Component decorator.

Open styles.css file located in src folder and add the following styles -

/* You can add global styles to this file, and also import other style files */

body {
  font-size: 18px;
  line-height: 1.58;
  background: #d53369;
  background: -webkit-linear-gradient(to left, #cbad6d, #d53369);
  background: linear-gradient(to left, #cbad6d, #d53369); 
  color: #333;
}

h1 {
  font-size: 36px;
}

* {
  box-sizing: border-box;
}

i {
  vertical-align: middle;
  color: #626262;
}

input {
  border: 1px solid #E8E8E8;
}

.page-title {
  text-align: center;
}

.todo-content {
  max-width: 650px;
  width: 100%;
  margin: 0 auto;
  margin-top: 60px;
  background-color: #fff;
  padding: 15px; 
  box-shadow: 0 0 4px rgba(0,0,0,.14), 0 4px 8px rgba(0,0,0,.28);
}

.form-control {
  font-size: 16px;
  padding-left: 15px;
  outline: none;
  border: 1px solid #E8E8E8;
}

.form-control:focus {
  border: 1px solid #626262;
}

.todo-content .form-control {
  width: 100%;
  height: 50px;
}

.todo-content .todo-create {
  padding-bottom: 30px;
  border-bottom: 1px solid #e8e8e8;
}

.todo-content .alert-danger {
  padding-left: 15px;
  font-size: 14px;
  color: red;
  padding-top: 5px;
}

.todo-content ul {
  list-style: none;
  margin: 0;
  padding: 0;
  max-height: 450px;
  padding-left: 15px;
  padding-right: 15px;
  margin-left: -15px;
  margin-right: -15px;
  overflow-y: scroll;
}

.todo-content ul li {
    padding-top: 10px;
    padding-bottom: 10px;
    border-bottom: 1px solid #E8E8E8;
}

.todo-content ul li span {
  display: inline-block;
  vertical-align: middle;
}

.todo-content .todo-title {
  width: calc(100% - 160px);
  margin-left: 10px;
  overflow: hidden;
  text-overflow: ellipsis;
}

.todo-content .todo-completed {
  display: inline-block;
  text-align: center;
  width:35px;
  height:35px;
  cursor: pointer;
}

.todo-content .todo-completed i {
  font-size: 20px;
}

.todo-content .todo-actions, .todo-content .edit-actions {
    float: right;
}

.todo-content .todo-actions i, .todo-content .edit-actions i {
    font-size: 17px;
}

.todo-content .todo-actions a, .todo-content .edit-actions a {
  display: inline-block;
  text-align: center;
  width: 35px;
  height: 35px;
  cursor: pointer;
}

.todo-content .todo-actions a:hover, .todo-content .edit-actions a:hover {
  background-color: #f4f4f4;
}

.todo-content .todo-edit input {
  width: calc(100% - 80px);
  height: 35px;
}
.todo-content .edit-actions {
  text-align: right;
}

.no-todos {
  text-align: center;
}

.toggle-completed-checkbox:before {
  content: 'check_box_outline_blank';
}

li.completed .toggle-completed-checkbox:before {
  content: 'check_box';
}

li.completed .todo-title {
  text-decoration: line-through;
  color: #757575;
}

li.completed i {
  color: #757575;
}

8. Add material-icons in index.html

We’re using material icons for displaying edit button, delete button and the checkbox. Just add the following <link> tag to src/index.html file -

  <link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">

9. Create TodoService todo.service.ts

The TodoService will be used to get the data from backend by calling spring boot apis. Create a new file todo.service.ts inside src/app directory and add the following code to it -

import { Injectable } from '@angular/core';
import { Todo } from './todo';
import { Headers, Http } from '@angular/http';
import 'rxjs/add/operator/toPromise';

@Injectable()
export class TodoService {
  private baseUrl = 'http://localhost:8080';

  constructor(private http: Http) { }

  getTodos():  Promise<Todo[]> {
    return this.http.get(this.baseUrl + '/api/todos/')
      .toPromise()
      .then(response => response.json() as Todo[])
      .catch(this.handleError);
  }

  createTodo(todoData: Todo): Promise<Todo> {
    return this.http.post(this.baseUrl + '/api/todos/', todoData)
      .toPromise().then(response => response.json() as Todo)
      .catch(this.handleError);
  }

  updateTodo(todoData: Todo): Promise<Todo> {
    return this.http.put(this.baseUrl + '/api/todos/' + todoData.id, todoData)
      .toPromise()
      .then(response => response.json() as Todo)
      .catch(this.handleError);
  }

  deleteTodo(id: string): Promise<any> {
    return this.http.delete(this.baseUrl + '/api/todos/' + id)
      .toPromise()
      .catch(this.handleError);
  }

  private handleError(error: any): Promise<any> {
    console.error('Some error occured', error);
    return Promise.reject(error.message || error);
  }
}

We need to declare TodoService inside app.module.ts to be able to use it in our components.

Open app.module.ts and add the following import statement -

// Inside app.module.ts
import { TodoService } from './todo.service';

Next, add TodoService inside the providers array -

// Inside app.module.ts
providers: [TodoService]

10. Implement TodoListComponent methods

Finally, We’ll implement methods for creating, retrieving, updating and deleting todos in our TodoListComponent.

Make sure that your todo-list.component.ts file matches with the following -

import { Component, Input, OnInit } from '@angular/core';
import { TodoService } from './todo.service';
import { Todo } from './todo';
import {NgForm} from '@angular/forms';

@Component({
  selector: 'todo-list',
  templateUrl: './todo-list.component.html'
})

export class TodoListComponent implements OnInit {
  todos: Todo[];
  newTodo: Todo = new Todo();
  editing: boolean = false;
  editingTodo: Todo = new Todo();

  constructor(
    private todoService: TodoService,
  ) {}

  ngOnInit(): void {
    this.getTodos();
  }

  getTodos(): void {
    this.todoService.getTodos()
      .then(todos => this.todos = todos );    
  }

  createTodo(todoForm: NgForm): void {
    this.todoService.createTodo(this.newTodo)
      .then(createTodo => {        
        todoForm.reset();
        this.newTodo = new Todo();
        this.todos.unshift(createTodo)
      });
  }

  deleteTodo(id: string): void {
    this.todoService.deleteTodo(id)
    .then(() => {
      this.todos = this.todos.filter(todo => todo.id != id);
    });
  }

  updateTodo(todoData: Todo): void {
    console.log(todoData);
    this.todoService.updateTodo(todoData)
    .then(updatedTodo => {
      let existingTodo = this.todos.find(todo => todo.id === updatedTodo.id);
      Object.assign(existingTodo, updatedTodo);
      this.clearEditing();
    });
  }

  toggleCompleted(todoData: Todo): void {
    todoData.completed = !todoData.completed;
    this.todoService.updateTodo(todoData)
    .then(updatedTodo => {
      let existingTodo = this.todos.find(todo => todo.id === updatedTodo.id);
      Object.assign(existingTodo, updatedTodo);
    });
  }

  editTodo(todoData: Todo): void {
    this.editing = true;
    Object.assign(this.editingTodo, todoData);
  }

  clearEditing(): void {
    this.editingTodo = new Todo();
    this.editing = false;
  }
}

Running Backend and Frontend Servers and Testing the app

You can run the spring boot backend server by typing mvn spring-boot:run in the terminal. It will run on port 8080.

$ cd spring-boot-backend
$ mvn spring-boot:run

The angular 4 frontend can be run by typing ng serve command. It will start on port 4200 -

$ cd angular4-frontend
$ ng serve --open

Enjoy working with the app now :-)

Conclusion

Whoa! The feeling that you get after building a fully fledged app from scratch is amazing. isn’t it?

Congratulations to you if you followed till the end and built the app successfully.

If you didn’t work with me step by step, then you can get the source code of the entire app from my github repository and work on it.

I know, this article was pretty long, and I did not go into much detail of all the topics. But you can always ask your doubts in the comment section below. I would be happy to help.

Thanks for reading folks. See you in the next blog post. Happy Coding!