Angular is one of the most popular JavaScript frameworks with incredible tooling, speed and performance.
If you wanted to learn how to use Angular and build a full stack application with Spring Boot in the back-end and Angular in the front-end then this blog post is for you.
In this post, We’ll build a fully-fledged todo app with Spring Boot & MongoDB in the back-end and Angular in the front-end.
Following is a screen shot 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.
- Head over to http://start.spring.io/.
- Enter Artifact’s value as todoapp.
- Add Spring Web and Spring Data MongoDB in the dependencies section.
- Click Generate to generate and download the project.
Once the project is downloaded, unzip it and import it into your favorite IDE. The Project’s directory structure should look like this -
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.
Todo
model
3. Creating the 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.NotBlank;
import javax.validation.constraints.Size;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
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.
TodoRepository
for accessing the database
4. Creating 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.
TodoController
5. Creating the APIs - 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.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 = Sort.by(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) {
return todoRepository.findById(id)
.map(todo -> ResponseEntity.ok().body(todo))
.orElse(ResponseEntity.notFound().build());
}
@PutMapping(value="/todos/{id}")
public ResponseEntity<Todo> updateTodo(@PathVariable("id") String id,
@Valid @RequestBody Todo todo) {
return todoRepository.findById(id)
.map(todoData -> {
todoData.setTitle(todo.getTitle());
todoData.setCompleted(todo.getCompleted());
Todo updatedTodo = todoRepository.save(todoData);
return ResponseEntity.ok().body(updatedTodo);
}).orElse(ResponseEntity.notFound().build());
}
@DeleteMapping(value="/todos/{id}")
public ResponseEntity<?> deleteTodo(@PathVariable("id") String id) {
return todoRepository.findById(id)
.map(todo -> {
todoRepository.deleteById(id);
return ResponseEntity.ok().build();
}).orElse(ResponseEntity.notFound().build());
}
}
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 frontend.
Creating the Angular Front-end
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 angular-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 angular-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 -
(app.component.html)
3. Change template for AppComponent 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.
todo.ts
4. Create Todo class 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 = false;
createdAt: Date = new Date();
}
todo-list.component.ts
5. Create TodoListComponent 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 { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
import { HttpClientModule } from '@angular/common/http';
import { AppComponent } from './app.component';
import { TodoListComponent } from './todo-list.component';
@NgModule({
declarations: [
AppComponent,
TodoListComponent
],
imports: [
BrowserModule,
FormsModule,
HttpClientModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
Note that, I have also added FormsModule
and HttpClientModule
inside app.module.ts
because we’ll need them for handling form-binding and calling the rest APIs respectively.
todo-list.component.html
6. Create template for TodoListComponent 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.
styles.css
7. Add Styles 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;
}
index.html
8. Add material-icons in 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">
todo.service.ts
9. Create TodoService 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 { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';
@Injectable()
export class TodoService {
private baseUrl = 'http://localhost:8080';
constructor(private http: HttpClient) { }
getTodos(): Observable<Todo[]> {
return this.http.get<Todo[]>(this.baseUrl + '/api/todos/')
.pipe(
catchError(this.handleError)
);
}
createTodo(todoData: Todo): Observable<Todo> {
return this.http.post<Todo>(this.baseUrl + '/api/todos/', todoData)
.pipe(
catchError(this.handleError)
);
}
updateTodo(todoData: Todo): Observable<Todo> {
return this.http.put<Todo>(this.baseUrl + '/api/todos/' + todoData.id, todoData)
.pipe(
catchError(this.handleError)
);
}
deleteTodo(id: string): Observable<any> {
return this.http.delete(this.baseUrl + '/api/todos/' + id)
.pipe(
catchError(this.handleError)
);
}
private handleError(error: HttpErrorResponse) {
if (error.status === 0) {
// A client-side or network error occurred. Handle it accordingly.
console.error('An error occurred:', error.error);
} else {
// The backend returned an unsuccessful response code.
// The response body may contain clues as to what went wrong.
console.error(
`Backend returned code ${error.status}, body was: `, error.error);
}
// Return an observable with a user-facing error message.
return throwError(
'Something bad happened; please try again later.');
}
}
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, 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()
.subscribe(todos => this.todos = todos );
}
createTodo(todoForm: NgForm): void {
this.todoService.createTodo(this.newTodo)
.subscribe(createTodo => {
todoForm.reset();
this.newTodo = new Todo();
this.todos.unshift(createTodo)
});
}
deleteTodo(id: string): void {
this.todoService.deleteTodo(id)
.subscribe(() => {
this.todos = this.todos.filter(todo => todo.id != id);
});
}
updateTodo(todoData: Todo): void {
console.log(todoData);
this.todoService.updateTodo(todoData)
.subscribe(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)
.subscribe(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 todoapp
$ mvn spring-boot:run
The Angular front-end can be run by typing ng serve
command. It will start on port 4200 -
$ cd angular-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!