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 introduced a new HttpClient service, which is a replacement for the Http service from Angular 2. It works mostly the same as the old service, handling both single and concurrent data loading with RxJs Observables, and writing data to an API.
Metal Toad is an AWS Managed Services provider. In addition to Angular+Django work we recommend checking out our article on how to host a website on AWS in 5 minutes.
Note to readers, May 18, 2018: the code in this post is built for Angular 5.x. The same techniques will work with Angular 6 as long as you use the rxjs-compat Node package. To see how to upgrade this code for full, native RxJS compatibility, see this post.
Angular 4.3 introduced a new HttpClient service, which is a replacement for the Http service from Angular 2. It works mostly the same as the old service, handling both single and concurrent data loading with RxJs Observables, and writing data to an API.
As of Angular 5.0, the older Http service still works, but it's deprecated and has been removed in Angular 6.0. The code samples in this post are compatible with Angular 4.3 and 5.x (and 6.x with rxjs-compat). If your project is still using Angular 4.2 or lower, including Angular 2, see my previous posts on making API calls with the Http service.
Many JavaScript developers should be familiar with using Promises to load data asynchronously. Observables are a more feature-rich system, which emit data in packets. A single Observable object can emit a single packet of data, or can emit a stream containing multiple discrete packets. Other objects can subscribe to these Observables and run a callback each time data is emitted. (In this example using the HttpClient service, each Observable will only emit data once, but a different type of Observable could emit data more than once.)
The Observable classes in Angular are provided by the ReactiveX library.
The HttpClient service in Angular 4.3+ is the successor to Angular 2's Http service and the $http service from AngularJS 1.x. Instead of returning a Promise, its http.get() method returns an Observable object.
The source code for this demo application is available on GitHub. That repository contains a simple API written in Express and a single-page Angular application which calls the API to read and write data.
This tutorial uses Webpack to manage assets. Angular 5 still supports SystemJS and you can use that instead if you prefer. For an example of how to configure SystemJS to work with HttpClient, see what it would look like if we upgraded my old Angular 2/4 demo app to Angular 5.
It's recommended that you try the Angular Tutorial first, for a basic overview of Angular architecture and Typescript.
The back-end for this app is a simple Express-based API that exposes the following endpoints:
GET /api/food
Returns an array of all existing Food objects in JSON format.
GET /api/books
Returns an array of all existing Book objects in JSON format.
GET /api/movies
Returns an array of all existing Movie objects in JSON format.
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.
The entire Express API code is as follows. (Portions omitted for brevity. See the sample app for the full code.)
const express = require('express'); const bodyParser = require('body-parser'); const path = require('path'); const app = express(); app.use(express.static(__dirname)); app.use(bodyParser.json()); // support json encoded bodies // some data for the API var foods = [ { "id": 1, "name": "Donuts" }, { "id": 2, "name": "Pizza" }, { "id": 3, "name": "Tacos" } ]; var books = [ { "title": "Hitchhiker's Guide to the Galaxy" }, { "title": "The Fellowship of the Ring" }, { "title": "Moby Dick" } ]; var movies = [ { "title": "Ghostbusters" }, { "title": "Star Wars" }, { "title": "Batman Begins" } ]; // the "index" route, which serves the Angular app app.get('/', function (req, res) { res.sendFile(path.join(__dirname,'/dist/index.html')) }); // the GET "books" API endpoint app.get('/api/books', function (req, res) { res.send(books); }); // the GET "movies" API endpoint app.get('/api/movies', function (req, res) { res.send(movies); }); // the GET "foods" API endpoint app.get('/api/food', function (req, res) { res.send(foods); }); // POST endpoint for creating a new food app.post('/api/food', function (req, res) { // calculate the next ID let id = 1; if (foods.length > 0) { let maximum = Math.max.apply(Math, foods.map(function (f) { return f.id; })); id = maximum + 1; } let new_food = {"id": id, "name": req.body.name}; foods.push(new_food); res.send(new_food); }); // PUT endpoint for editing food app.put('/api/food/:id', function (req, res) { let id = req.params.id; let f = foods.find(x => x.id == id); f.name = req.body.name; res.send(f); }); // DELETE endpoint for deleting food app.delete('/api/food/:id', function (req, res) { let id = req.params.id; let f = foods.find(x => x.id == id); foods = foods.filter(x => x.id != id); res.send(f); }); // HTTP listener app.listen(3000, function () { console.log('Example listening on port 3000!'); }); module.exports = app;
To use the Angular HttpClient, we need to inject it into our app's dependencies:
import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { HttpClientModule } from '@angular/common/http'; // replaces previous Http service import { FormsModule } from '@angular/forms'; import { DemoService } from './demo.service'; // our custom service, see below import { AppComponent } from './app.component'; @NgModule({ imports: [BrowserModule, FormsModule, HttpClientModule], declarations: [AppComponent], providers: [DemoService], schemas: [CUSTOM_ELEMENTS_SCHEMA], bootstrap: [AppComponent] }) export class AppModule { }
Our demo app contains only one simple component, which contains a few elements to display some simple data. We will be loading data from a few JSON files, to simulate an API call.
src/app/app.component.ts:
import {Component} from '@angular/core'; import {DemoService} from './demo.service'; import {Observable} from 'rxjs/Rx'; @Component({ selector: 'demo-app', template:` <h1>Angular 5 HttpClient Demo App</h1> <h2>Foods</h2> <ul> <li *ngFor="let food of foods">{{food.name}}</li> </ul> ` }) export class AppComponent { public foods; constructor(private _demoService: DemoService) { } }
We can use HttpClient to request a single resource, by using http.get. This is very similar to the Angular 2 Http service. Notice that we no longer have to `.map((res:Response) => res.json()` because HttpClient handles this for us:
src/app/demo.service.ts:
import {Injectable} from '@angular/core'; import { HttpClient, HttpHeaders } from '@angular/common/http'; import {Observable} from 'rxjs/Observable'; const httpOptions = { headers: new HttpHeaders({ 'Content-Type': 'application/json' }) }; @Injectable() export class DemoService { constructor(private http:HttpClient) {} // Uses http.get() to load data from a single API endpoint getFoods() { return this.http.get('/api/food'); } }
Our Demo service makes the HTTP request and returns the Observable object. To actually get the data from the service, we need to update our component to subscribe to the Observable:
src/app/app.component.ts:
... ngOnInit() { this.getFoods(); } + getFoods() { + this._demoService.getFoods().subscribe( + data => { this.foods = data}, + err => console.error(err), + () => console.log('done loading foods') + ); + } }
The subscribe() method takes three arguments which are event handlers. They are called onNext, onError, and onCompleted. The onNext method will receive the HTTP response data. Observables support streams of data and can call this event handler multiple times. In the case of the HTTP request, however, the Observable will usually emit the whole data set in one call. The onError event handler is called if the HTTP request returns an error code such as a 404. The onCompleted event handler executes after the Observable has finished returning all its data. This is less useful in the case of the Http.get() call, because all the data we need is passed into the onNext handler.
For more information about the Observable object, see the ReactiveX documentation.
In our example here, we use the onNext handler to populate the component's 'foods' variable.
The error handler just logs the error to the console. The completion callback runs after the success callback is finished.
The handler functions are optional. If you don't need the error or completion handler, you may omit them. If you don't provide an error handler, however, you may end up with an uncaught Error object which will stop execution of your application.
Many times, we need to load data from more than one source, and we need to delay the post-loading logic until all the data has loaded. ReactiveX Observables provide a method called forkJoin() to wrap multiple Observables. Its subscribe() method sets the handlers on the entire set of Observables.
To run the concurrent HTTP requests, let's add the following code to our service:
src/app/demo.service.ts:
... @Injectable() export class DemoService { constructor(private http:HttpClient) {} // Uses http.get() to load data from a single API endpoint getFoods() { return this.http.get('/api/food'); } + // 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('/api/books'), + this.http.get('/api/movies') + ); + } }
Notice that forkJoin() takes multiple arguments of type Observable. These can be Http.get() calls or any other asynchronous operation which implements the Observable pattern. We don't subscribe to each of these Observables individually. Instead, we subscribe to the "container" Observable object created by forkJoin().
When using Http.get() and Observable.forkJoin() together, the onNext handler will execute only once, and only after all HTTP requests complete successfully. It will receive an array containing the combined response data from all requests. In this case, our books data will be stored in data[0] and our movies data will be stored in data[1].
The onError handler here will run if either of the HTTP requests returns an error code.
Next, we subscribe to the new method in our component:
src/app/app.component.ts:
import {Component} from '@angular/core'; import {DemoService} from './demo.service'; import {Observable} from 'rxjs/Rx'; @Component({ selector: 'demo-app', template:` <h1>Angular 5 HttpClient Demo App</h1> <p>This is a complete mini-CRUD application using an Express back-end. See src/app/demo.service.ts for the API call code.</p> <h2>Foods</h2> <ul> <li *ngFor="let food of foods">{{food.name}}</li> </ul> + <h2>Books and Movies</h2> + <p>This is an example of loading data from multiple endpoints using Observable.forkJoin(). The API calls here are read-only.</p> + <h3>Books</h3> + <ul> + <li *ngFor="let book of books">{{book.title}}</li> + </ul> + <h3>Movies</h3> + <ul> + <li *ngFor="let movie of movies">{{movie.title}}</li> + </ul> ` }) export class AppComponent { public foods; + public books; + public movies; ... getFoods() { ... } + getBooksAndMovies() { + this._demoService.getBooksAndMovies().subscribe( + data => { + this.books = data[0] + this.movies = data[1] + } + // No error or completion callbacks here. They are optional, but + // you will get console errors if the Observable is in an error state. + ); + } }
To write data to our API, we need to add several new methods to our DemoService class:
src/app/demo.service.ts:
... @Injectable() export class DemoService { constructor(private http:HttpClient) {} ... + createFood(food) { + let body = JSON.stringify(food); + return this.http.post('/api/food/', body, httpOptions); + } + + updateFood(food) { + let body = JSON.stringify(food); + return this.http.put('/api/food/' + food.id, body, httpOptions); + } + + deleteFood(food) { + return this.http.delete('/api/food/' + food.id); + } }
Notice that our createFood(), updateFood(), and deleteFood() methods use API endpoints which return the saved object in JSON format. Returning the object when creating, updating, or deleting is a nice convenience for the developer of the front-end application. Not all APIs return this data. 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.
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.
Now that we have the service in place, we can add some basic CRUD features to our AppComponent.
src/app/app.component.ts:
import {Component} from '@angular/core'; import {DemoService} from './demo.service'; import {Observable} from 'rxjs/Rx'; @Component({ selector: 'demo-app', template:` <h1>Angular 5 HttpClient Demo App</h1> <p>This is a complete mini-CRUD application using an Express back-end. See src/app/demo.service.ts for the API call code.</p> <h2>Foods</h2> <ul> - <li *ngFor="let food of foods">{{food.name}}</li> + <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="food_name" [(ngModel)]="food_name"><button (click)="createFood(food_name)">Save</button></p> <h2>Books and Movies</h2> ... ` }) export class AppComponent { public foods; public books; public movies; + public food_name; ... getFoods() { ... } getBooksAndMovies() { ... } + 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); + } + ); + } + } }
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 another Angular component.
It's theoretically possible, but I wouldn't.
If you use forkJoin() to run multiple data-saving requests, it could have unpredictable results. forkJoin() will cancel all the requests if the first one returns an error. However, if one request completes successfully while a later one fails, your data could end up in a broken or partially-saved state. It seems best to use parallel API calls only when reading data.
To write multiple types of data to an API, try one of the following workflows:
See the next part in my series of posts about Angular API calls, showing API authentication using Django Rest Framework and JSON Web Tokens.
Looking for more examples of Angular API calls using HttpClient and ForkJoin? See how I used Angular, Django Rest Framework, HttpClient, and ForkJoin to rebuild a classic text adventure game.
See how to upgrade the code samples in this post for full, native compatibility with RxJS 6
Happy coding!
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...
How to run multiple concurrent HTTP requests, with the callbacks running only after all of them have completed with Angular 2.
With the release of Angular 6.0 in May 2018, the framework has been updated to depend on version 6.0 of the RxJ
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.