TODO List Project - Backend Part IV

previous_step

Connecting Angular Frontend to Spring Boot Restful Services 2 - Designing RESTful Services for Todo Resource

RESTful Services for Todo Resource includes:

  • Retrieve all Todos for A User
    GET /users/{user_name}/todos
  • DELETE a Todo of a User
    DELETE /users/{user_name}/todos/{todo_id}
  • EDIT/UPDATE a Todo
    PUT /users/{user_name}/todos/{todo_id}
  • CREATE a new Todo
    POST /users/{user_name}/todos/

1. Creating Retrieve Todo List Request

1.1 Creating RESTful API for Retrieving Todo List

@CrossOrigin(origins = "http://localhost:4200")
@RestController
public class TodoResource {
    
  @Autowired
  private TodoHardcodedService todoService;

  @GetMapping("/users/{username}/todos")
  public List<Todo> getAllTodos(@PathVariable String username) {
    return todoService.findAll();
  }
}

1.2 Connecting Angular Frontend with Todo List RESTful Service

  • First, Creating a todo data service
export class TodoDataService {
  constructor (
    private http: HttpClient
  ) {}

  retrieveAllTodos(username) {
    return this.http.get<Todo[]>(`http://localhost:8080/users/${username}/todos`);
  }
}
  • Second, get todo List and display it in Frontend. In ListTodosComponent, we call the todo data service get the todo list. And we want to load the list when this component is loaded, so in ngOnInit(), we call the data service.
export class Todo {
  constructor (
    public id: number,
    public description: string,
    public done: boolean,
    public targetDate: Date
    ) {
  }
}

export class ListTodosComponent implements OnInit {
	
  todos: Todo[]

  constructor (
    private todoService: TodoDataService
  ) {}

  ngOnInit() {
    this.todoService.retrieveAllTodos('teemo').subscribe(
      response => {
        this.todos = response;
      }
    )
  }
}

2. Creating DELETE Request Method

2.1 Creating RESTful API

@DeleteMapping("/users/{username}/todos/{id}")
public ResponseEntity<Void> deleteTodo (@PathVariable String username, @PathVariable long id) {
  Todo todo = todoService.deleteById(id);
  if (todo != null)
    return ResponseEntity.noContent().build();
  else
    return ResponseEntity.notFound().build();
}

Note that the return type of this delete controller is ResponseEntity<>. This is because we want to return a specific status of the content back. There are two options: either we can return success or we can return no content back. We choose to return noContent() if the DELETE operation is successful, otherwise we are returning a not found status back.

ResponseEntiey helps us to build specific requests with specific state assigned.

2.2 Adding DELETE Todo Feature to Angular Frontend

  • First, we add delete request in todo-data service.
deleteTodo(username, id) {
  return this.http.delete(`http://localhost:8080/users/${username}/todos/${id}`);
}
  • Second, we add a delete button in list todos component. When showing the todo list we use <tr *ngFor="let todo of todos">, so using todo.id we can get the id of todo.
<button (click)='deleteTodo(todo.id)' class="btn btn-warning">Delete Todo</button>
  • Third, add delete todo logic in list-todos component.ts. Note that after deleting a todo, we want to refresh the list again. So we refactor our code.
ngOnInit() { this.refreshTodos() }
refreshTodos() {
  this.todoService.retrieveAllTodos('teemo').subscribe(
    response => {
      console.log(response)
      this.todos = response
  }
)
}

deleteTodo(id) {
  //console.log(`Delete Todo ${id}`);
  this.todoService.deleteTodo('teemo', id).subscribe(
    response => {
      console.log(response);
      this.message = `Delete of Todo ${id} Successful!`;
      this.refreshTodos();
    }
  )
}

3. Creating Update Request Method

In this part, we design our front end at first.

3.1 Creating Todo Component and Handle Routing

In list-todos.component.html, adding a update button.

<button (click)='updateTodo(todo.id)' class="btn btn-success">Update Todo</button>

When we click this Update button, we want to open a new edit page. So, we create a component called todo. We use it to update todo and also create a new todo. ng generate component todo.

Next, we add Routing to TodoComponent.

  • declare the routing to TodoComponent in app-routing.module.ts. That is
    { path:'todos/:id', component: TodoComponent, canActivate: [RouteGuardService]}.
  • inject Router in ListTodosComponent and use the router to navigate to TodoComponent.
    constructor(
      private todoService: TodoDataService,
      private router: Router
    ) { }
    updateTodo(id) {
      //console.log(`Update Todo ${id}`);
      this.router.navigate(['todos',id]);
    }

3.2 Designing Todo Page with Bootstrap Framework

<h1>Todo</h1>

<div class="container">
  <fieldset class="form-group">
    <label>Description</label>
    <input type="text" class="form-control" name="description" required="required">
  </fieldset>

  <fieldset class="form-group">
    <label>Target Date</label>
    <input type="date" class="form-control" name="targetDate" required="required">
  </fieldset>

  <button class="btn btn-success" (click)="saveTodo()">Save</button>
</div>

3.3 Creating Retrieve Todo and Connect Angular Frontend

  • First, design the RESTful API for retrieving a specific todo by id
@GetMapping("/users/{username}/todos/{id}")
public Todo getTodo(@PathVariable String username, @PathVariable long id) {
  return todoService.findById(id);
}
  • Second, add request in Angular todo-data.service.ts
retrieveTodo (username, id) {
  return this.http.get<Todo>(`http://localhost:8080/users/${username}/todos/${id}`);
}
  • Third, make use of the above service in TodoComponent. We need to pass id into method retrieveTodo. How can we get this id? We read it in the URL using route.
export class TodoComponent implements OnInit {

  todo: Todo;

  id: number;

  constructor(
    private todoService: TodoDataService,
    private route: ActivatedRoute
  ) { }

  ngOnInit() {
    this.id = this.route.snapshot.params['id'];
    this.todoService.retrieveTodo('teemo', this.id).subscribe(
      data => { this.todo = data; }
    )
}

This is not sufficient, we would need to map the todo.description and todo.targetDate to the View. We can use [(ngModel)]="todo.description".

<input type="text" [(ngModel)]="todo.description" class="form-control" name="description" required="required">
<input type="date" [(ngModel)]="todo.targetDate" class="form-control" name="targetDate" required="required">

But by now, we still have some issues, first is TodoComponent.html:7 ERROR TypeError: Cannot read property 'description' of undefined, and second is data format issue, i.e. The specified value "2019-06-09T19:51:51.147+0000" does not conform to the required format, "yyyy-MM-dd".

For the first issue, we need to look at how we are loading the data for this todo component. We are using a .subscribe. So we are asynchronously calling this. So this update method say it’s OK when the data is available do this. But our ngOninit() method is completed execution. It would directly start loading up the todo.component.html. So at that point the object might be null, and that’s the reason why todo that description is giving an error. To solve it, we create a initial todo object.

To solve the date format issue, we use pipe. But for [(ngModel)], it is used to display specific model element. We can not do [(ngModel)]="todo.targetDate | date". Therefore, what we do is first we split [(ngModel)] into two parts. The first one is property binding, and the second one is event binding. Then we applied pipe in the property binding.

<input type="date" 
    [ngModel]="todo.targetDate | date:'yyyy-MM-dd'" 
    (ngModelChange)="todo.targetDate = $event"
    class="form-control" name="targetDate" required="required">

3.4 Creating RESTful API for PUT Request Method

@PutMapping("/users/{username}/todos/{id}")
public ResponseEntity<Todo> updateTodo(@PathVariable String username, @PathVariable long id, @RequestBody Todo todo) {
  Todo todoUpdated = todoService.save(todo);
  return new ResponseEntity<Todo>(todo, HttpStatus.OK);
}

3.5 Creating RESTful API for POST Request Method

What we want to do for a created resource is the location of the created resource. So what we would typically return for a created post request, is what is the url of the created resource.

@PostMapping("/users/{username}/todos")
public ResponseEntity<Void> updateTodo (@Pathvariable String username, @RequestBody Todo todo) {
  Todo createdTodo = todoService.save(todo);

  // Location 
  // Get current resource url and concate {id}
  URI uri = ServletUriComponnetsBuider.fromCurrentRequest().path("/{id}").buildAndExpand(createdTodo.getId()).toUri();

  return ResponseEntity.created(uri).build();
}

Now, when we call this request, in the response we can get a Location, i.e. http://localhost:8080/users/teemo/todos/{id}. And when we visit this uri, we can get the createdTodo.

Till now, the complete code of the backend in Spring Boot is as follows.

CLICK ME
@CrossOrigin(origins = "http://localhost:4200")
@RestController
public class TodoResource {
    
  @Autowired
  private TodoHardcodedService todoService;

  @GetMapping("/users/{username}/todos")
  public List<Todo> getAllTodos(@PathVariable String username) {
    return todoService.findAll();
  }

  @GetMapping("/users/{username}/todos/{id}")
  public Todo getTodo(@PathVariable String username, @PathVariable long id) {
    return todoService.findById(id);
  }

  @DeleteMapping("/users/{username}/todos/{id}")
  public ResponseEntity<Void> deleteTodo (@PathVariable String username, @PathVariable long id) {
    Todo todo = todoService.deleteById(id);
    if (todo != null)
      return ResponseEntity.noContent().build();
    else
      return ResponseEntity.notFound().build();
  }

  @PutMapping("/users/{username}/todos/{id}")
  public ResponseEntity<Todo> updateTodo(@PathVariable String username, @PathVariable long id, @RequestBody Todo todo) {
    Todo todoUpdated = todoService.save(todo);

    return new ResponseEntity<Todo>(todo, HttpStatus.OK);
  }

  @PostMapping("/users/{username}/todos")
  public ResponseEntity<Void> updateTodo(@PathVariable String username, @RequestBody Todo todo) {
    Todo createdTodo = todoService.save(todo);

    //Location
    //Get current resource url
    URI uri = ServletUriComponentsBuilder.fromCurrentRequest().path("/{id}").buildAndExpand(createdTodo.getId()).toUri();

    return ResponseEntity.created(uri).build();
  }
}

We did the Delete, the Put and the Post, what we’re trying to do in here is we are trying to follow the HTTP standards. The Delete method is trying to return nothing if it is successful. And if it’s failing, then it would return not found. The PUT request would return a status of OK with the content of the updated resource and the created resource when you tried to do a POST, it returns the status as created, and returns the URL of the created resource.

Next, we will design the frontend of put and post request.

4. RESTful Web Services - Bset Practices

Before we go further, let’s evaluate a few best practices in RESTful services design.

  • The first and best is CONSUMER FIRST. Last but not least have great documentation for your apps. Swagger is one of the most popular documentation standard for RESTful APIs.
  • MAKE BEST USE OF HTTP. Use the right request method appropriate for your specific action and ensure you are sending a proper response status back when a resource is not found do not send a server an error, send 400. When you create a resource do not send just success, send created back.
  • NO SECURE INFO IN URI.
  • USE PLURALS. E.g.
    • Prefer /users to /user
    • Prefer /users/1 to /user/1
  • USE NOUNS FOR RESOURCES, FOR EXCEPTIONS: DEFINE A CONSISTENT APPROACH. E.g.
    • /search
    • PUT gists/{id}/star
    • DELETE /gists/{id}/star

5. Implementing Update Todo Feature in Angular Frontend

5.1 Adding PUT request in todo-data.service.ts

Notice we also need to pass in the BODY.

updateTodo (username, id, todo) {
  return this.http.put(`http://localhost:8080/users/${username}/todos/${id}`, todo);
}

5.2 Editing TodoComponent

  • First, binding an event on the Save button.
  • Second, coding method saveTodo()
saveTodo () {
  this.todoService.updateTodo('teemo', this.id, this.todo).subscribe(
    response => {
      console.log(response);
      this.router.navigate(['todos']);
    }
  );
}

6. Implementing New Todo Feature in Angular Frontend

6.1 Adding a button in ListTodosComponent to navigate to TodoComponent

addTodo() {
  this.router.navigate(['todos', -1])
}

6.2 Adding POST request in todo-data.service.ts

creasteTodo (username, todo) {
  return this.http.post(`http://localhost:8080/users/${username}/todos`, todo);
}

6.3 Making a call in TodoComponent

Binding an event saveTodo() to the Save button. That is (click)="saveTodo()". The code in saveTodo() is shown as follows.

saveTodo() {
  if (this.id == -1) {  // === is used to compare objects ; == compare primitive types
    //Create Todo
    this.todoService.creasteTodo('teemo', this.todo).subscribe(
      response => {
        console.log(response);
        this.router.navigate(['todos']);
      }
    );

  } else {
    // update Todo
    this.todoService.updateTodo('teemo', this.id, this.todo).subscribe(
      response => {
        console.log(response);
        this.router.navigate(['todos']);
      }
    );
  }
}

6.4 Improving Todo Form - Validation and Form Submit on Enter - ngSubmit

Next, we improve the form on two aspects. One is when we press enter, we can submit the form. Two, when we press save, we sould have basic validation.

Let’s first look at the code in todo.component.html again.

CLICK ME
<h1>Todo</h1>

<div class="container">
  
  <fieldset class="form-group">
    <label>Description</label>
    <input type="text" [(ngModel)]="todo.description" class="form-control" name="description" required="required">
  </fieldset>

  <fieldset class="form-group">
    <label>Target Date</label>
    <input type="date" 
    [ngModel]="todo.targetDate | date:'yyyy-MM-dd'" 
    (ngModelChange)="todo.targetDate = $event"
    class="form-control" name="targetDate" required="required">
  </fieldset>

  <button class="btn btn-success" (click)="saveTodo()">Save</button>
</div>

We can see there is no form in our .html file. Actually, we don’t really need a form in Angular, because we are not really submitting anything out of the server. But when you want to utilize the form features, like when I pressed enter inside any of the fields, I would want to submit the form. In those kind of situations we would need a form.

If we use a <form> and hava a submit button inside the form, i.e. add an attribute type in the button, we’ll not really need to handle the button click event. We will make it on the form. So the form we will add a submit event will say (ngSubmit)="saveTodo()".

Let’s consider the second question? How can we check this form is valid in Angular?
In angular, there are some things called template variables. #todoForm="ngForm" let the whole form be a template variables. In Angular, it provides many features to the form, e.g. .inValid.

Now, the code in todo.component.html will be

CLICK ME
<h1>Todo</h1>

<div class="container">
  <form (ngSubmit)="!todoForm.invalid && saveTodo()" #todoForm="ngForm">
    <fieldset class="form-group">
      <label>Description</label>
      <input type="text" [(ngModel)]="todo.description" class="form-control" 
      name="description" required="required" minlength="5">
    </fieldset>

    <fieldset class="form-group">
      <label>Target Date</label>
      <input type="date" 
      [ngModel]="todo.targetDate | date:'yyyy-MM-dd'" 
      (ngModelChange)="todo.targetDate = $event"
      class="form-control" name="targetDate" required="required">
    </fieldset>

    <button type="submit" class="btn btn-success">Save</button>
  </form>
</div>

6.5 Enhancing Validation Messages on Todo Page

  • Adding styles to the invalid part in the form. The code in todo.component.css is

    .ng-invalid:not(form) {
        border-left: 5px solid red;
    }
  • Add alert message and only display this message when a field is edited.

<div class="alert alert-warning" *ngIf="todoForm.dirty && todoForm.invalid">Enter valid values</div>

Also we can add template variables to other elements in the form, such as #targetDate="ngModel. <div class="alert alert-warning" *ngIf="todoForm.dirty && targetDate.invalid">Enter valid target date</div>

next_step

   Reprint policy


《TODO List Project - Backend Part IV》 by Tong Shi is licensed under a Creative Commons Attribution 4.0 International License
  TOC