Services
Along with components and directives, services are one of the main building blocks of an Angular application.
A component's only concern should be displaying data and not managing it. Services are where the Angular team advocates placing the business logic and data management of the application. Having a clear separation between the presentation layer and the other processings of the application increases reusability and modularity.
Creating a service with the CLI is done as follows:
ng generate service services/example
This will create the service and its associated test class in the app/services
folder. It is a common practice to place services in a services folder, the CLI will create the folder if it doesn't already exists.
The following content is automatically generated in the example.service.ts
file:
import { Injectable } from '@angular/core'
@Injectable({
providedIn: 'root'
})
export class ExampleService {
}
When a component requires a service, the service should be added to its constructor in the following manner:
import { Component } from '@angular/core'
import { ExampleService } from '@services/example.service'
@Component({
selector: 'app-example',
templateUrl: './example.component.html'
})
export class ExampleComponent {
constructor(
private exampleService: ExampleService
) {}
}
TIP
Always declare a dependency to a service as private. Indeed the template should never directly access a service but always go through a property or a method exposed by the component class.
Dependency Injection
In the previous chapter, we've injected services provided by the @angular/router
library in the components needing them. If you are familiar with Spring, you may not have thought much about it as it's one of the framework's mechanisms.
At bootstrap, Angular creates an application-wide injector. If other injectors are needed, Angular will create them along the way. The injector creates dependencies (most often in the form of services), and maintains a container of dependency instances that it reuses if possible. The injector gets the information about how to create or retrieve a dependency from a provider. A service usually acts as its own provider.
You may not have realised, but we have already used providers. In the pipe chapter, to use the UpperCasePipe
in the component class instead of in the template, we added it to the providers array of the component.
When Angular discovers that a component depends on a service, it first checks if the injector has any existing instances of that service. If a requested service instance doesn't yet exist, the injector makes one using the registered provider, and adds it to the injector before returning the service to Angular. When all requested services have been resolved and returned, Angular can call the component's constructor with those services as arguments.
Dependencies can be provided at three levels:
- root level: this is the default behaviour when creating a service with the CLI. That is what
providedIn: 'root'
means. The same instance of the dependency is injected everywhere it is needed as if it were a singleton. - module level: the dependency is added to the providers array of the
NgModule
. The module gets its own instance of the dependency - component level: the dependency is added to the providers array of the component. Each instance of that component gets its own instance of the dependency.
Practical Work: State management
- Generate an
AuthenticationService
with the CLI in theapp/services
folder - Move the
loggedIn
logic from theAppComponent
to the service - Inject the service in the
LoginFormComponent
and use it. - Implement a logout method in the authentication service and add a logout button in the
AppComponent
that calls it and navigates back to theLoginFormComponent
. Here is the html and css:
<button class="logout">Logout</button>
<router-outlet></router-outlet>
.logout {
align-self: end;
}
- Conditionally show the Logout button depending on the
loggedIn
status of the user - Use a navigation guard to redirect the user who wants to access the film search page to
/login
if they are not authenticated (make the CanActivate return true if the route can be accessed else return aUrlTree
via thecreateUrlTree
method of theRouter
service). To future-proof the guard, add a returnUrl as a queryParam to the returnedUrlTree
so that theLoginFormComponent
knows where to navigate back to after authentication and modify theLoginFormComponent
accordingly. To generate the navigation guard use the following CLI command:
ng generate guard guards/authentication
# ? Which interfaces would you like to implement? CanActivate
Help for the UrlTree
this.router.createUrlTree(['/login'], { queryParams: { returnUrl: state.url }})
Expected result
The HttpClient
In a Single Page Application (SPA), communication with the server is done via asynchronous HTTP requests (AJAX) or more specialized protocols such as WebSocket. We will see how to make these network requests from an Angular application.
Angular provides a module, the HttpClientModule
, to make HTTP calls. The module provides an injectable service, the HttpClient
, to make GET, POST, PATCH, DELETE and PUT requests. To inject the HttpClient
in a service, first add the HttpClientModule
to the AppModule
's imports
array.
Here are a few examples:
import { Injectable } from '@angular/core'
import { HttpClient } from '@angular/common/http'
import { Observable } from 'rxjs'
import { User } from '@models/user/user'
import { UserCreation } from '@models/user/user-creation'
import { UserEdition } from '@models/user/user-edition'
@Injectable({
providedIn: 'root'
})
export class UserService {
private baseUrl = 'api/backoffice/users'
constructor(private httpClient: HttpClient) {}
public create(user: UserCreation): Observable<User> {
return this.httpClient.post<User>(this.baseUrl, user)
}
public update(ref: string, user: UserEdition): Observable<User> {
return this.httpClient.put<User>(`${this.baseUrl}/${ref}`, user)
}
public getByUserReference(ref: string): Observable<User> {
return this.httpClient.get<User>(`${this.baseUrl}/${ref}`)
}
}
import { Component } from '@angular/core'
import { User } from '@models/user/user'
import { UserService } from '@services/user.service'
@Component({
selector: 'app-user',
templateUrl: './user.component.html'
})
export class UserComponent {
user: User | null = null
reference = ''
constructor(private userService: UserService) {}
getUser(): void {
this.userService.getByUserReference(this.reference))
.subscribe(user => this.user = user)
}
}
The methods from the HttpClient
service return Observables. They will be covered in the next chapter about the RxJS library. An Observable is only executed once subscribed to via the subscribe
method. The subscribe method is expecting at least one callback. It is most often provided as an arrow function.
Practical Work: Calling a backend
We will use an API (the backend) to authenticate users and search for films. This backend has already been developed and can be deployed locally using the following command line commands (clone in your usual repo folder):
git clone https://github.com/worldline/vuejs-training-backend.git
cd vuejs-training-backend
npm install
npm start
The npm start
command will ask you for an API key. Either wait on the instructor to give it to you, alternatively, you can generate one here
TIP
The backend interface contract is available here: api-docs
- Add to the
src
folder either theproxy.conf.json
file if you are not behind a corporate proxy or theproxy.conf.js
file.
{
"/api/*": {
"target": "http://localhost:3030",
"changeOrigin": true,
"pathRewrite": {
"^/api": ""
}
}
}
var HttpsProxyAgent = require('https-proxy-agent')
var proxyConfig = [{
context: '/api',
target: 'http://localhost:3030',
changeOrigin: true,
pathRewrite: {
"^/api": ""
}
}]
function setupForCorporateProxy(proxyConfig) {
var proxyServer = process.env.http_proxy || process.env.HTTP_PROXY
if (proxyServer) {
var agent = new HttpsProxyAgent(proxyServer);
console.log('Using corporate proxy server: ' + proxyServer)
proxyConfig.forEach(function(entry) {
entry.agent = agent
})
}
return proxyConfig
}
module.exports = setupForCorporateProxy(proxyConfig)
The proxy will divert all calls for http://localhost:4200/api to the server running at http://localhost:3030. It also makes sure we don't encounter any CORS issues (in the case the backend is not hosted on your machine). This configuration is only for the webpack dev server the CLI provides to run the application on your machine in a dev environment. It will not be the configuration used in production.
- Install the following dependency only if you are behind a corporate proxy
npm install --save-dev https-proxy-agent
- In the CLI configuration file -
angular.json
- add theproxyConfig
option to the serve target:
...
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
...
"options": {
"proxyConfig": "src/proxy.conf.json" // or "src/proxy.conf.js"
},
"defaultConfiguration": "development"
},
...
- Add the
HttpClientModule
to theAppModule
'simports
array. If VSCode is unable to find the import, add the following line manually at the top of theapp.module.ts
file:
import { HttpClientModule } from '@angular/common/http'
- Create the interfaces/classes for the models used by the backend, add one file per model in the
models/authentication
folder:
export class RegistrationRequest {
constructor(
public email: string,
public password: string,
public firstname: string,
public lastname: string
) {}
}
export class LoginRequest {
constructor(
public email: string,
public password: string
) {}
}
import { User } from './user'
export class UserResponse {
constructor(
public user: User,
public token: string
) {}
}
/* eslint-disable @typescript-eslint/naming-convention */
export class User {
constructor(
public id: number,
public firstname: string,
public lastname: string,
public email: string,
public created_at: string,
public update_at: string
) {}
}
Note the token in the UserResponse
, it will serve to authenticate the user via the Authorization header: Authorization: Bearer <token>
. Learn more about JWT here.
- Implement the
register
andlogin
methods in theAuthenticationService
as follows:
private baseUrl = 'api/user'
constructor(private httpClient: HttpClient) {}
login(loginRequest: LoginRequest): Observable<UserResponse> {
return this.httpClient.post<UserResponse>(`${this.baseUrl}/login`, loginRequest)
}
register(loginRequest: LoginRequest): Observable<UserResponse> {
const registrationRequest = new RegistrationRequest(
loginRequest.email,
loginRequest.password,
'John',
'Smith'
)
return this.httpClient.post<UserResponse>(`${this.baseUrl}/register`, registrationRequest)
}
- The change in the call signature of the
login
method will require a bit of refactoring in theLoginFormComponent
:
constructor(
private router: Router,
private route: ActivatedRoute,
private authenticationService: AuthenticationService
) {}
login(): void {
this.authenticationService.login(this.loginRequest)
.subscribe({ next: () => {
const returnUrl = this.route.snapshot.queryParamMap.get('returnUrl')
this.router.navigateByUrl(returnUrl ? `/${returnUrl}` : '')
} })
}
register(): void {
this.authenticationService.register(this.loginRequest)
.subscribe()
}
get loginRequest(): LoginRequest {
return new LoginRequest(this.email, this.password)
}
- Refactoring is also needed to keep the
AuthenticationGuard
working. Make theloggedIn
boolean in theAuthenticationService
depend on atoken
field and make theLoginFormComponent
save the token that it gets from the login call in that field.
token: string | null = null
get loggedIn(): boolean {
return this.token != null
}
login(): void {
this.authenticationService.login(this.loginRequest)
.subscribe(response => {
this.authenticationService.token = response.token
const returnUrl = this.route.snapshot.paramMap.get('returnUrl')
this.router.navigateByUrl(returnUrl ? `/${returnUrl}` : '')
})
}
- Add a register button next to the login button in the
LoginFormComponent
, give it the attributetype="button"
so that Angular knows it is not this button that triggers thengSubmit
event on the form and make it call the register method. You should now be able to register a user and login.
<div class="button-container">
<button type="button">Register</button>
<button type="submit">Login</button>
</div>
.button-container {
display: flex;
justify-content: space-between;
}
- It is time to handle errors. The subscribe method can be passed an object that takes three callbacks: a next, an error and a complete (we will look at this in more details in the next chapter). Declare an
errorMessage
field on theLoginFormComponent
and update it with the error information retrieved from the argument of theerror
callback. Display the error message on the form. Check that the error message is actually shown when you login with incorrect credentials.
private errorHandler(errorResponse: HttpErrorResponse): void {
this.errorMessage = errorResponse.error.error ?? `${errorResponse.error.status} - ${errorResponse.error.statusText}`
}
// subscribe syntax
this.authenticationService.login(this.loginRequest)
.subscribe({
next: (userResponse) => { /* */},
error: (errorResponse) => { /* */ }
})
hint
For a better UX (User eXperience), do not forget to clear the errorMessage
field before launching a new login or registration request or as soon as a register or login succeed.
- Let's now call the backend to get the list of films. The route is secured which means that passing the token in the header is necessary. Angular provides a mechanism - http interceptors - to systematically intercept http requests, allowing to define the headers in one place.
a. Use the CLI to generate one: ng generate interceptor interceptors/authentication
.
b. Here is its implementation:
import { Injectable } from '@angular/core'
import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor } from '@angular/common/http'
import { Observable } from 'rxjs'
import { AuthenticationService } from '@services/authentication.service'
@Injectable()
export class AuthenticationInterceptor implements HttpInterceptor {
constructor(private authenticationService: AuthenticationService) {}
intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
const token = this.authenticationService.token
if (token) {
const cloned = request.clone({
headers: request.headers.set('Authorization', `Bearer ${this.authenticationService.token}`)
})
return next.handle(cloned)
}
return next.handle(request)
}
}
If there is a token in the AuthenticationService
, the interceptor will add it to the http request's headers.
c. Add the interceptor to the providers of the AppModule
providers: [
{ provide: HTTP_INTERCEPTORS, useClass: AuthenticationInterceptor, multi: true }
],
- Create a
FilmService
using the CLI and implement the call to theapi/movies/search
endpoint. Note that thetitle
queryParam is not optional. To add query params to a request, use theoptions
parameter of the get method.
const options = {
params: new HttpParams().set('title', title)
}
Make changes to the
FilmSearchComponent
to call this new service with the title filled in by the user, save the response to thefilms
field in theFilmSearchComponent
.Check that the token is sent as a HTTP header via the developer tools of your browser.
Bonus: Modify the
AuthenticationService
logout method so that it sets the token tonull
.