-
Notifications
You must be signed in to change notification settings - Fork 1
Angular HTTP
Topics Covered:
Sending POST Request
GET Request
Transforming Output
Creating a Loading Spinner
Using service for HTTP Request
DELETE Request
Handling Errors
Setting Headers
Adding Query Params
Response Types
Changing Response Body
Interceptors
Request Interceptors
Response Interceptors
Multiple Interceptors
Summary
-
In the
app.module
file, add a new core Angular moduleHttpClientModule
which is found in@angular/common/http
.imports: [ BrowserModule, FormsModule, ReactiveFormsModule, AppRoutingModule, HttpClientModule ],
-
This now unlocks the HTTP client and with that, we can now start sending requests in our app component here.
-
Now inject our
HttpClient
in the component where you want to POST the request,constructor(private http: HttpClient) {}
-
We can use that injected HTTP service that offers couple of methods: GET, POST, PUT and DELETE
-
We can call POST method the API endpoint and data that needs to be send:
this.http .post<{name: string}>('https://angular-practice-166c4.firebaseio.com/posts.json', postData)
-
Note: it is important to keep in mind that you never communicate directly with a database from inside your Angular app. You always connect with an API hosted in backend which in turns communicate with database.
-
You normally send JSON data when interacting with a RESTful API and actually, that will happen here as well, the Angular
HttpClient
will take our JavaScript object and automatically convert it to JSON data. -
If you try to submit the request here, it won't work. Angular heavily uses observable. HTTP requests are also managed via observable, so we have to
subscribe
to them to get informed about the response and to handle errors and so on.this.http .post<{name: string}>('https://angular-practice-166c4.firebaseio.com/posts.json', postData) .subscribe(responseData => { console.log(responseData); });
-
Let's have a look at the network tab, we actually see two requests send to the post endpoint. Now that's just a characteristic of browsers when sending post requests.
- The first one is of type options, that will first of all check whether the post request is allowed to be sent and if that gets a success response, it will send the actual request
- The second one is the actual request which will be send/cancelled based on response of first request
-
We will use GET method to fetch data from API. We need just one argument which is URI of endpoint.
this.http .get<{[key: string]: Post}>('https://angular-practice-166c4.firebaseio.com/posts.json')
-
All four methods of HTTP are generic, you can specify them what type of data is to be send/fetched. In example above, we receive data in key-value pair where key is of type string and value is of type Object (here POST is not the POST method but a custom obj. with name and id property).
-
Like POST request, you need to subscribe to this request else it won't work
this.http .get<{[key: string]: Post}>('https://angular-practice-166c4.firebaseio.com/posts.json') .subscribe(posts => { console.log('data: ', posts); });
-
Transforming data would generally not be a problem but it is a good practice to use observable operators because it simply allows us to write cleaner code with different steps we funnel our data through that can easily be swapped or adjusted, so that you have a lean subscribe function here and have other steps that focus on other parts.
-
Before we
subscribe()
, we can callpipe()
becausepipe()
allows you to funnel your observable data through multiple operators before they reach thesubscribe()
method. -
We can use
map()
operator that allows us to get some data and return new data which is then automatically re-wrapped into an observable so that we can still subscribe to it, if it would not be wrapped into an observable again, we could not subscribe. -
Here the idea is that we return an array of posts instead of an object:
this.http .get<{[key: string]: Post}>('https://angular-practice-166c4.firebaseio.com/posts.json') .pipe( map(responseData => { console.log('data: ', responseData); //object const postArray: Post[] = []; for (const key in responseData) { if (responseData.hasOwnProperty(key)) { postArray.push({ id: key, ...responseData[key] }); } } return postArray; }) ) .subscribe(posts => { console.log('data: ', posts); //array });
- Create a property
isLoading
which initially isfalse
i.e data fetching from service is off and as soon as we start fetching data from service, set it totrue
. We could have other logic running parallel which will keep track of this property which if set totrue
will display some loading message and as soon it switches tofalse
, loading message will be overridden by actual response data.loadedPosts: Post[] = []; // array to store and display data isFetching: boolean = false; fetchRequest() { this.http .get<{[key: string]: Post}>('https://angular-practice-166c4.firebaseio.com/posts.json') .pipe( map(responseData => { this.isFetching = true; // message is loading, show loading message ... }) ) .subscribe(posts => { this.isFetching = false; // message loaded successfully, show response data this.loadedPosts = posts; // store response in array, loop this in template }, error => { this.errorMessage = error.message; this.isFetching = false; // message loaded unsuccessfully, show error data console.log('GET Error: ', this.errorMessage); }); }
-
In bigger applications, we already have a quite big component with a lot of code that is only indirectly related to the user interface.
-
Of course, we want to send a request when a button is clicked then we want to display that response data but for example, transforming the result, transforming the data, we can do it here, it's not inherently bad but it is a nice practice to outsource that into services.
-
So services are the parts in your Angular application that do the heavy lifting work and your components are relatively lean and are mostly concerned with template related work.
-
We have two ways to process response data that is fetched from service
-
The first alternative is that we use a
subject
in the service where wenext(responseData)
our response data when we got them and we subscribe to that subject in the component. This scenario would be perfect if we have multiple components interested in the response data. -
The second alternative is to simply return the result of our request (e.g. result from
get()
method) and that would be our observable. So I don't want to subscribe here in service, instead I only return the prepared observable here from service I have to subscribe in the app component.
-
-
So now we moved the result handling into the component but the more heavy lifting, the part detached from the template and from the UI which is the sending of the request and the transformation of the data, that now lives in the service.
-
This is a best practice when working with Angular and HTTP requests - Move the part that is related to your template, into the component and be informed about the result of your HTTP request by subscribing in the component but move the rest into the service and simply return the observable there so that you set up everything in the service but you can subscribe in the component.
-
If your component doesn't care about the response and about whether the request is done or not, then there is no reason to subscribe in the component, then you can just subscribe in the service. But if it does care about the response and the response status, then having that service component split is great.
-
This method requires a URL to which it should send the request:
this.http .delete('https://angular-practice-166c4.firebaseio.com/posts.json') .subscribe(responseData => {...});
OR
return this.http .delete('https://angular-practice-166c4.firebaseio.com/posts.json'); // IN COMPONENT deleteServiceMethod().subscribe(responseData => {...});
-
Now if I want to be informed about that deletion process in the component, I will return my observable and I will not subscribe here in the service but instead now in the app component. Else we can simply subscribe that in service.
-
Scenario: You're not allowed to read, you can still write.
-
If you make a GET request now, you will have multiple console errors. It is good practice to handle errors so that users will know that something went wrong, usually they don't check console for any errors or delay in response, so it is us to handle error and display error/descriptive message.
-
Approach 1: Handle error after subscription:
errorMessage = null; ... private fetchPost(): void { this.isFetching = true; this.postService.fetchPost() .subscribe( posts => { this.isFetching = false; this.loadedPosts = posts; }, error => { this.errorMessage = error.message; this.isFetching = false; }); }
-
We can use this variable
errorMessage
to display message in UI<div class="col-xs-12 col-md-6 col-md-offset-3"> <ul class="list-group" *ngIf="loadedPosts.length > 0 && !isFetching; else error"> <li class="list-group-item" *ngFor="let post of loadedPosts"> <h3>{{ post.title }}</h3> <p>{{ post.content }}</p> </li> </ul> <ng-template #error> <p *ngIf="loadedPosts.length < 1 && !isFetching">No posts available!</p> <p *ngIf="isFetching && !errorMessage">Loading...</p> </ng-template> <div class="alert alert-danger" *ngIf="errorMessage"> <h3>Error Occured</h3> <p>{{ errorMessage }}</p> </div> </div>
-
Note: I have used Firebase here, the Firebase server is sending back a customized error response. Now the important thing is you will get that HTTP error response object by Angular and it will have an error key but the detailed content of what's in there depends on the API you're talking to!
-
Firebase gives you an object with another error key and that permission denied message. Your own API might not be sending this or might be sending different data, so it's always important to understand which API you're working with and what this API sends back in the case of a success message or in the case of an error.
-
If you need more information about the error and not just a generic message, then you can dive into that error object you're getting. You can also get information about the headers, exact status code that was thrown which can be very helpful in showing a more useful message and so on.
-
-
Approach 2: Using Subjects to handle errors
- This approach is useful in cases like when you send a request and don't subscribe to it in your component. You can react to error in service itself wherein you could use a
subject()
and that is especially useful if you have multiple places in the application that might be interested in your error.
errorMessage = new Subject(); ... createAndStorePost(post: Post) { this.http .post<{name: string}>('https://angular-practice-166c4.firebaseio.com/posts.json', post) .subscribe( responseData => { ... }, error => { this.errorMessage.next(error.message); } ); }
- Now the remaining step is that we subscribe to that subject in all the places we're interested in that error message.
errorSubscription: Subscription = null; ... ngOnInit(): void { this.errorSubscription = this.postService.errorMessage.subscribe(message => { console.log('POST Error: ', message); this.errorMessage = message; }); } ... ngOnDestroy(): void { this.errorSubscription.unsubscribe(); }
- This approach is useful in cases like when you send a request and don't subscribe to it in your component. You can react to error in service itself wherein you could use a
-
Approach 3: Using
catchError
- Let's say when we fetch posts, where we already pipe some data, we got an error and we want to handle that. Now we can simply add the
catchError
operator, in here you could now do stuff like send to analytics server or some generic error handling task you might want to do that maybe is not related to the UI - Although you could use the
subject()
andnext()
the error message here too but maybe you have some behind the scenes stuff you want to do when an error occurs, log it somewhere, send it to your own server, your analytics server, anything like that. - And once you're done handing that error, you should pass it on though, it definitely needs to be able to reach subscribe, just as you need to pass something here in map as well.
return this.http .get<{[key: string]: Post}>('https://angular-practice-166c4.firebaseio.com/posts.json') .pipe( map(responseData => { ... return responseData; }), catchError(errorMsg => { console.log('catchError: ', errorMsg.message); return throwError(errorMsg); }) );
- Let's say when we fetch posts, where we already pipe some data, we got an error and we want to handle that. Now we can simply add the
-
Exit From Error Message in UI
- In the error message block, we could add a button, where we say OK or anything like this and this is simply there for the user to get rid of that error message
... <div class="alert alert-danger" *ngIf="errorMessage"> <h3>Error Occured</h3> <p>{{ errorMessage }}</p> <button class="btn btn-danger" (click)="handleError()">Exit</button> </div>
handleError(): void { this.errorMessage = null; }
-
When configuring these HTTP requests, you set things like the URL you send the request, set the data you want to attach to your request, set some special headers, for example when you have a back-end that requires authorization and looks for an authorization header or if you want to set your own content type or you need to attach a custom header because your API you are sending the request to needs it.
-
Now setting your own headers is extremely simple, any HTTP request method, no matter if it's POST or GET or DELETE or PUT or whatever, any of these methods has an extra last argument, which is an object where you can configure that request.
-
To configure headers, you need to import a HTTP headers object from
@angular/common/http
, and this allows you to create a new instance of this object with thenew
keyword and to this object, you can pass a JavaScript object here with the object literal notation where you can have key-value pairs of your headers and headers are of course key-value pairs.this.http .post<{name: string}>('https://angular-practice-166c4.firebaseio.com/posts.json', post, { headers: new HttpHeaders({ 'Custom-Header': 'Aakash', 'Access-Control-Allow-Origin': '*'}) } )
- You set parameters by adding the
params
key here in that same config object where you addedheaders
and you setparams
equal tonew HttpParams()
. Just as HTTP headers, you importHttpParams
from@angular/common/http
.ORreturn this.http .get<{[key: string]: Post}>('https://angular-practice-166c4.firebaseio.com/posts.json', { params: new HttpParams().set('print', 'pretty').set('custom-param', 'aakash') }); /* same as: https://angular-practice-166c4.firebaseio.com/posts.json?print=pretty&custom-param=aakash */
let searchParams = new HttpParams(); searchParams = searchParams.append('print', 'pretty'); searchParams = searchParams.append('custom-param', 'aakash'); return this.http .get<{[key: string]: Post}>('https://angular-practice-166c4.firebaseio.com/posts.json', { params: searchParams }); /* same as: https://angular-practice-166c4.firebaseio.com/posts.json?print=pretty&custom-param=aakash */
- Important,
searchParams
, this object is immutable, so you actually need to append the results usingappend()
method.
-
There can be scenarios where you need access to the entire response object and not just to the extracted body data, sometimes you need to find out which status code was attached or you need some response headers and in such cases, you can change the way the Angular
HttpClient
parses that response and you can basically tell Angular, "hey, please don't give me the unpacked, the extracted response data you found in the body, give me the full response instead" -
One such example is the
observe
that takes a couple of values (body
,response
,events
etc.)- the default and
body
means that you get that response data extracted and converted to a JavaScript object automatically. -
response
option gets back the full HTTP response object.this.http .post<{name: string}>('https://angular-practice-166c4.firebaseio.com/posts.json', post, { headers: new HttpHeaders({ 'Custom-Header': 'Aakash', 'Access-Control-Allow-Origin': '*'}), observe: 'response' })
- With observe set to
events
, it gives you very granular control over how you update the UI and in which phase your request currently is!
- the default and
-
Working with events
-
Let's simply have a look at this events thing by using one other operator which we can chain in here and we import that from
rxjs/operators
and that's thetap()
operator and that simply allows us to execute some code without altering the response, so that we basically just can do something with the response but not disturb our subscribe function and the functions we passed as arguments to subscribe.return this.http .delete('https://angular-practice-166c4.firebaseio.com/posts.json', { observe: 'events', responseType: 'text' }).pipe( tap(events => { console.log('Events from delete request: ', events); if (events.type === HttpEventType.Response) { console.log('tap response: ', events.body); } if (events.type === HttpEventType.Sent) { // update UI } }) );
-
If we console log events, we get two outputs - the first one logs an empty object or an almost empty object where we have type 0 and the second one is the HTTP response object. In the end, you have different type of events and they are encoded with numbers.
-
However here in code, you don't have to use these numbers, you have a more convenient way. We can check event type and compare this to
HttpEventType
which is anenum
you can import from@angular/common/http
. -
More on this topic
- You can not only configure the observe mode here, you can also configure the response type. The default is JSON. You could however configure response type to be text or blob etc.
return this.http .get<{[key: string]: Post}>('https://angular-practice-166c4.firebaseio.com/posts.json', { responseType: 'json', params: new HttpParams().set('print', 'pretty').set('custom-param', 'aakash') })
-
We're sending our HTTP requests here and whenever we want to configure something like
params
, we're doing this on a per request basis and oftentimes, this is the way it should be because every request might need different headers. But let's imagine we want to attach this custom header to all our outgoing requests and a more realistic scenario would be that you want to authenticate your user and you need to add a certain param or a certain header to every outgoing request therefore so that the back-end can read that, you don't want to manually configure every request because that is very cumbersome and for that, you can add interceptors. -
Interceptor will run code before your request leaves your app
-
Creating Interceptors:
- Create a Service and make that implement
HttpInterceptor
- This interface will force you to implement
intercept()
method that takes two parameters:- the first one is a
request
object, which is of type HTTP request - second argument which is passed to intercept is
next
, that accepts request and propagates it to further channels. - This function will forward the request because the interceptor will basically run code before your request leaves your app, so before it really is sent and right before the response is forwarded to subscribe, so it steps into that request flow and next is a function you need to call to let the request continue its journey but more on that in a second too.
- This intercept method now allows us to run code right before the request leaves our application.
- the first one is a
-
If you don't return next handle and pass the request, then the request will not continue and therefore your app will basically break, you definitely have to return this here:
export class AuthInterceptorService implements HttpInterceptor { intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { console.log('intercepting request...'); return next.handle(req); } }
-
Now with that, we have to provide that service and we have to provide this in a special way. E.g. in
app.module
add a new element to the providers array and that should be a JavaScript object where you have three keys -- The first key is the
provide
key and there, you have to useHTTP_INTERCEPTORS
, this is the token by which this injection can later be identified by Angular, so it will basically know that all the classes you provide on that token, so by using that identifier, should be treated as HTTP interceptors and should therefore run their intercept method whenever a request leaves the application. - The second key you pass to that object is the use class key where you now point at your interceptor class you want to add as an interceptor and here, that would be the
AuthInterceptorService
- And last is
multi
, you can have more than one interceptor and you inform Angular about that and that it should not replace the existing interceptors with this one by adding multi and setting this to true.
providers: [Service_1, Service_2, { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptorService, multi: true } ]
- The first key is the
-
Now Angular will do the rest, it will automatically grab all your HTTP interceptors and run their intercept method whenever a request leaves the application.
-
Inside of an interceptor, you can not only log data, you can also modify the request object. However, the request object itself is immutable, so you can't set request URL to a new URL, that will not work and you also get an error here.
-
Instead if you want to modify the request, you have to create a new one and call request
clone()
method and inside of clone, you pass in a JavaScript object where you now can overwrite all the core things. You could set a new URL here or you could add new headers, if you want to keep the old headers by the way, then you simply do that by using the request headers and calling append or you add new params or whatever you want.intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { const modifiedRequest = req.clone({ headers: req.headers.append('Access-Control-Allow-Origin', '*').append('Auth', 'aakash') }); return next.handle(modifiedRequest); }
-
The important thing here is, I do not forward the original request but my
modifiedRequest
and that of course is a typical use case for an interceptor, you change the request and then you forward that modified request.
-
You're also not limited to interacting with the request in an interceptor, you can also interact with the response. You do this by adding something here to handle because handle actually gives you an observable, which I guess makes sense because in the end, your request is an observable to which you subscribe in the end.
-
So this in the end is the request with the response in it, wrapped into an observable and you can add pipe and do something with the response if you want to e.g. you could add the map operator here to change the response.
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { console.log('Logging Interceptor started...', req.headers); return next.handle(req).pipe(tap( events => { if (events.type === HttpEventType.Response) { console.log('response tapped from interceptor ', events.body); } } )); }
-
The order in which you provide multiple interceptors matters because that will be the order in which they are executed.
-
Now this
AuthInterceptorService
will run first and thereafter comes theLoggingInterceptorService
. Now often, the order might not matter but if it does, be aware of that order and order your interceptors accordingly so that the order in which they execute fits your use case and whatever you're doing in your interceptors.providers: [Service_1, Service_2, { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptorService, multi: true }, { provide: HTTP_INTERCEPTORS, useClass: LoggingInterceptorService, multi: true } ],
-
The most common task you will do is to send requests by using POST, GET and so on by adding a URL, possibly a request body and maybe some request headers.
-
Always remember that these methods, like POST and GET give you back an observable to which you have to subscribe to actually kick of the request and to send it. Since it is an observable, you can use operators to transform the data, log data, catch errors or whatever you want to do but ultimately, you need to subscribe and you can do this in a service or in a component depending on where you need to work with the response and in the subscribe method, you pass in a function to handle the data you get back and possibly also an error handling function.
-
Now you've got advanced features, like the possibility of setting what you want to
observe
if you are interested in the response or just the body of the response or all the events, which type of data should be returned, -
You can set query params and you have that very useful features, interceptors which can really help you save time and code if you have some header that needs to be appended to every outgoing request.