AngularJS

Angular 2: Using the HTTP Service to Write Data to an API

Update, November 27, 2017: This post explains the Http service used in Angular 2. This is now deprecated in favor of the newer HttpClient released in Angular 4.3.


Filed under:

Update, November 27, 2017: This post explains the Http service used in Angular 2. This is now deprecated in favor of the newer HttpClient released in Angular 4.3. This post will remain here as long as Angular 4.x is in long term support. If you are using Angular 5, consider upgrading to the newer HttpClient. You can find a tutorial for the HttpClient service in my post Angular 5: Making API calls with the HttpClient service.

In my previous article, Angular 2: HTTP, Observables, and concurrent data loading, we investigated querying data from an API endpoint using Angular 2's Http service and the Observable pattern. In this second article, we will look at using Http to save data to our API endpoint.

Consider the Angular 2 service we created in the previous article, DemoService:

import {Injectable} from '@angular/core';
import {Http, Response} from '@angular/http';
import {Observable} from 'rxjs/Rx';
@Injectable()
export class DemoService {
constructor(private http:Http) { }
  // Uses http.get() to load a single JSON file
  getFoods() {
    return this.http.get('/app/food.json').map((res:Response) => res.json());
  }
  // Uses Observable.forkJoin() to run multiple concurrent http.get() requests.
  // The entire operation will result in an error state if any single request fails.
  getBooksAndMovies() {
    return Observable.forkJoin(
      this.http.get('/app/books.json').map((res:Response) => res.json()),
      this.http.get('/app/movies.json').map((res:Response) => res.json())
    );
  }
}

The back-end API

The next step is to handle the other HTTP verbs: POST, PUT, and DELETE. Unlike our original GET requests from Part 1, these requests require a live API backend. You can use Node, Drupal, Django, or the back-end framework of your choice to create this API. The actual API creation is out of scope for this article.

For this article, the demo app contains a simple Express API that has the following REST endpoints:

POST /api/food

Creates a new Food object in the back-end data store

Accepts a JSON object in the request body.
If successful, returns a 200 OK response, containing a JSON object representing the data as saved on the server, including the auto-numbered ID

PUT /api/food/{food_id}

Updates an existing Food object

Accepts a JSON object in the request body.
If successful, returns a 200 OK response, containing a JSON object representing the data as saved on the server.

DELETE /api/food/{food_id}

Deletes an existing Food object

Does not require a response body.
If successful, returns a 200 OK response, containing a JSON object representing the Food object as it existed before it was deleted.

Not all APIs return the same data and formats. Some may return a different status code, some XML data, or nothing at all. Consult the documentation for your API to determine what the response format will look like.

To communicate with the API, we add several new methods to our DemoService class:

import {Injectable} from "@angular/core";
import {Http, Response, Headers, RequestOptions} from "@angular/http";
import {Observable} from "rxjs/Rx";
@Injectable()
export class DemoService {
  …
  createFood(food) {
    let headers = new Headers({ 'Content-Type': 'application/json' });
    let options = new RequestOptions({ headers: headers });
    let body = JSON.stringify(food);
    return this.http.post('/api/food/', body, options ).map((res: Response) => res.json());
  }
  updateFood(food) {
    let headers = new Headers({ 'Content-Type': 'application/json' });
    let options = new RequestOptions({ headers: headers });
    let body = JSON.stringify(food);
    return this.http.put('/api/food/' + food_id, body, options ).map((res: Response) => res.json());
  }
  deleteFood(food_id) {
    return this.http.delete('/api/food/' + food_id);
  }
}

Notice that our createFood() and updateFood() methods use API endpoints which return the saved object in JSON form. Thus we need to use .map((res: Response) => res.json()) to make the JSON objects easily available to the HTTP Observable's subscribers.

The DELETE method of our API returns nothing, so we don't use the .map() method.

If we didn't do this, the subscribers would receive a Response object instead. This is more difficult to work with, and defeats our goal of abstracting the HTTP logic within the service.

Possible bug in Firefox

At the time of this writing, the API calls on one of my projects were failing when run in Firefox. It seems that Angular 2 was not sending the Content-type: application/json headers with the requests. If your API supports this, you might be able to work around the problem by changing your API URLs to include the .json extension (e.g., /api/food/1.json).

This seems to be Firefox specific and does not affect Chrome or Edge.

Creating and Saving Data from our Components

Now that we have the service in place, we can add some basic CRUD features to our AppComponent:

...
 
@Component({
  selector: 'demo-app',
  template:`
  <h1>Angular2 HTTP Demo App</h1>
  <h2>Foods</h2>
  <ul>
    <li *ngFor="let food of foods"><input type="text" name="food-name" [(ngModel)]="food.name"><button (click)="updateFood(food)">Save</button> <button (click)="deleteFood(food)">Delete</button></li>
  </ul>
  <p>Create a new food: <input type="text" name="new_food" [(ngModel)]="new_food"><button (click)="createFood(new_food)">Save</button></p>
  <h2>Books and Movies</h2>
  ...
  `
})
export class AppComponent {
 
  public foods;
  public books;
  public movies;
 
  public new_food;
 
  ...
 
  createFood(name) {
    let food = {name: name};
    this._demoService.createFood(food).subscribe(
       data => {
         // refresh the list
         this.getFoods();
         return true;
       },
       error => {
         console.error("Error saving food!");
         return Observable.throw(error);
       }
    );
  }
 
  updateFood(food) {
    this._demoService.updateFood(food).subscribe(
       data => {
         // refresh the list
         this.getFoods();
         return true;
       },
       error => {
         console.error("Error saving food!");
         return Observable.throw(error);
       }
    );
  }
 
  deleteFood(food) {
    if (confirm("Are you sure you want to delete " + food.name + "?")) {
      this._demoService.deleteFood(food).subscribe(
         data => {
           // refresh the list
           this.getFoods();
           return true;
         },
         error => {
           console.error("Error deleting food!");
           return Observable.throw(error);
         }
      );
    }
  }
}

Note: For brevity, I omitted some code that has not changed since the previous article. See GitHub for the full source code.

You'll notice that we added some basic form fields and buttons to the template, and new methods createFood(), updateFood(), and deleteFood() to the component class. These are called when users click the buttons in the template, and handle saving and deleting the data.

For simplicity, I have used a simple JavaScript confirm() dialog as a delete confirmation. An enhancement might be to implement a nicer-looking dialog using promises.

What about Observable.ForkJoin()?

In the previous post, we used the forkJoin() method to run multiple simultaneous GET requests. We could theoretically do the same when saving data, but it would be difficult to do this safely. If one request completes successfully while another request fails, your data could end up in a broken or partially-saved state.

You would be better off passing a single, larger data object to your back-end API, which could then wrap all the saving logic in a database transaction for better data integrity.

Happy coding!

Similar posts

Get notified on new marketing insights

Be the first to know about new B2B SaaS Marketing insights to build or refine your marketing function with the tools and knowledge of today’s industry.