Tutorials

Angular 4 Services Tutorial

Key Takeaways

  • Angular services let components share reusable logic and state through dependency injection.
  • A service isn't inherently a singleton. Root providers share an application-level instance; component and route providers can create separate instances.
  • The standalone example below creates SessionService, injects it into a component, and displays Hello Sam without an AppModule.
  • Shared in-memory state isn't persistent session storage, and a shared HTTP service doesn't automatically cache requests.

Angular services are classes that hold logic or state you don't want to duplicate across components. Dependency injection, or DI, lets a component request a service instead of constructing and managing that dependency itself. Where you provide the service determines which consumers share its instance.

Create and Inject an Angular Service

1. Create a standalone project

Install Node.js and npm using a Node.js release listed in Angular's compatibility table. If the tooling is unfamiliar, start with our Angular CLI installation guide.

npx @angular/cli@22.2.3 new angular-service-example --standalone --routing=false --ssr=false --style=css --test-runner=vitest --defaults
cd angular-service-example

This creates a standalone application with tests. The CLI's current naming convention uses files such as src/app/app.ts and src/app/app.html, rather than app.component.ts and app.component.html. Standalone applications don't need an app.module.ts; see the project-generation options.

2. Generate SessionService

npx ng generate service session --type=service --injectable

The command creates src/app/session.service.ts and src/app/session.service.spec.ts. The explicit --type=service preserves the familiar filename and SessionService class name.

There's an important change from older tutorials: the current generator defaults to @Service(), Angular's shorthand for a root-provided service. The --injectable option deliberately selects @Injectable so this tutorial can show providedIn explicitly. Both approaches support the root-sharing pattern used here. See the service generator and service creation guide.

3. Add shared state and reusable behavior

Replace src/app/session.service.ts with:

import { Injectable, signal } from '@angular/core';

@Injectable({ providedIn: 'root' })
export class SessionService {
  private readonly userNameState = signal('Sam');
  readonly userName = this.userNameState.asReadonly();

  setUserName(name: string): void {
    this.userNameState.set(name);
  }
}

A signal is a value Angular can track for changes. Call userName() to read it; the service updates its writable signal with set(). Exposing asReadonly() lets consumers read changes while keeping writes behind the service's method.

The Injectable decorator supplies metadata Angular uses to construct injectable classes and resolve their dependencies. Its providedIn: 'root' option also registers this service with the application's root injector—the object responsible for resolving application-level dependencies. You don't need to add SessionService to a module or application providers array.

A service can inject other services and injectable configuration values. Keep component-specific UI behavior in components rather than making an application service depend on a component instance.

4. Inject the service into a component

Replace src/app/app.ts with:

import { Component, inject } from '@angular/core';
import { SessionService } from './session.service';

@Component({
  selector: 'app-root',
  standalone: true,
  templateUrl: './app.html',
})
export class App {
  readonly session = inject(SessionService);
  readonly currentUser = this.session.userName;
}

Replace src/app/app.html with:

<p>Hello {{ currentUser() }}</p>
<button type="button" (click)="session.setUserName('Alex')">
  Change user
</button>

inject(SessionService) requests the dependency from Angular. A field initializer is a supported injection context. The component keeps a reference to the signal, not a one-time copy of its current string value, so subsequent changes remain visible. Every field is initialized at declaration.

5. Bootstrap and run the application

Keep the generated app.config.ts. The relevant contents of src/main.ts should be:

import { bootstrapApplication } from '@angular/platform-browser';
import { App } from './app/app';
import { appConfig } from './app/app.config';

bootstrapApplication(App, appConfig)
  .catch((error: unknown) => console.error(error));

bootstrapApplication() starts the standalone root component using the application configuration. No manual SessionService registration is required.

npx ng serve

Open the address printed by the development server—normally localhost:4200. The page initially displays Hello Sam; selecting Change user displays Hello Alex.

Angular Service Scope: Root Versus Component Providers

Think of a provider as an instruction telling an injector how to obtain a dependency. Consumers that resolve SessionService through the same root provider share that instance. A nearer provider can override that resolution and create a separate instance. Angular's hierarchical injector guide explains the lookup rules.

Provider locationSharing and lifetimeTypical use
providedIn: 'root'One shared instance from that application's root injector, unless another provider overrides it.Application-wide user state or reusable data-access logic.
Route providersA router-created environment injector supplies the route and its children.State or dependencies limited to a feature's route subtree.
Component providersA separate instance for each providing component instance; destroyed with that component.Independent editors, drafts, or widgets.

Route providers belong to an environment injector, not to each component instance. Don't assume their lifetime matches one component's lifetime; see the Route providers API. In an existing NgModule application, module providers remain available, but a root-provided service still doesn't need redundant registration there.

Conceptual service ownership

Application root injector: SessionService A
  |-- Header component -------- uses A
  |-- Profile component ------- uses A
  `-- IsolatedSession component
        local provider: SessionService B
        `-- local consumers --- use B

Create an intentionally separate instance

For an optional contrast, create src/app/isolated-session.ts:

import { Component, inject } from '@angular/core';
import { SessionService } from './session.service';

@Component({
  selector: 'app-isolated-session',
  standalone: true,
  providers: [SessionService],
  template: `
    <p>Local user: {{ session.userName() }}</p>
    <button type="button" (click)="session.setUserName('Taylor')">
      Change local user
    </button>
  `,
})
export class IsolatedSession {
  readonly session = inject(SessionService);
}

To render it, import IsolatedSession in app.ts, add it to the component's imports array, and place <app-isolated-session></app-isolated-session> in app.html. Its local provider creates an independent SessionService. Changing its name doesn't change the root service's name; rendering two IsolatedSession components gives each its own instance.

Sharing State Isn't Persisting a Session

SessionService currently stores only in-memory display state. Reloading the page creates a new application and resets the name to Sam. Sharing an instance doesn't synchronize browser tabs, persist data, or authenticate a user.

Test the Service and Its Consumer

Run these tests against the original App and SessionService example before adding optional components. The project setup selects Vitest, and Angular's TestBed provides an isolated DI environment for service and component tests.

Test service behavior and instance sharing

Replace src/app/session.service.spec.ts with:

import { TestBed } from '@angular/core/testing';
import { describe, expect, it } from 'vitest';
import { SessionService } from './session.service';

 describe('SessionService', () => {
  it('shares state through the same injector', () => {
    TestBed.configureTestingModule({});
    const first = TestBed.inject(SessionService);
    const second = TestBed.inject(SessionService);

    expect(first.userName()).toBe('Sam');
    expect(second).toBe(first);

    first.setUserName('Alex');
    expect(second.userName()).toBe('Alex');
  });
});

Replace the dependency with a component-test stub

A stub supplies predictable behavior without using the real service. Replace src/app/app.spec.ts with:

import { signal } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { describe, expect, it } from 'vitest';
import { App } from './app';
import { SessionService } from './session.service';

 describe('App', () => {
  it('renders and updates the injected user name', async () => {
    const name = signal('Alex');
    const stub: Pick<SessionService, 'userName' | 'setUserName'> = {
      userName: name.asReadonly(),
      setUserName: (value: string) => name.set(value),
    };

    TestBed.configureTestingModule({
      imports: [App],
      providers: [{ provide: SessionService, useValue: stub }],
    });

    const fixture = TestBed.createComponent(App);
    await fixture.whenStable();
    const element: HTMLElement = fixture.nativeElement;
    expect(element.querySelector('p')?.textContent).toBe('Hello Alex');

    stub.setUserName('Jordan');
    await fixture.whenStable();
    expect(element.querySelector('p')?.textContent).toBe('Hello Jordan');
  });
});

The useValue provider substitutes the stub. Waiting for fixture.whenStable() allows rendering and scheduled updates to finish before assertions. Component-level providers need separate attention when overriding dependencies; Angular's component testing scenarios covers that distinction.

npx ng test --watch=false

The --watch=false option runs the tests without entering watch mode.

Expose Asynchronous Data Through a Service

API data arrives later, so consumers need loading, success, and error states. Angular's HttpClient returns Observables—streams that deliver results when subscribed. A shared service instance doesn't automatically share or cache those requests: separate subscriptions can issue separate requests.

For a runnable HTTP exercise without a backend, create public/current-user.json. The CLI's default asset configuration serves files from public. This fixture isn't an authentication endpoint.

{ "userName": "Sam" }

Create src/app/user-api.service.ts:

import { HttpClient } from '@angular/common/http';
import { Injectable, inject } from '@angular/core';
import { Observable } from 'rxjs';

export interface CurrentUser {
  userName: string;
}

@Injectable({ providedIn: 'root' })
export class UserApiService {
  private readonly http = inject(HttpClient);

  getUser(): Observable<CurrentUser> {
    return this.http.get<CurrentUser>('/current-user.json');
  }
}

In the Angular version used here, HttpClient is available for injection by default. The provideHttpClient() helper configures HTTP features when needed; see HttpClient setup.

Create src/app/user-profile.ts:

import { AsyncPipe } from '@angular/common';
import { Component, inject } from '@angular/core';
import { catchError, map, of, startWith } from 'rxjs';
import { UserApiService } from './user-api.service';

type ProfileState =
  | { status: 'loading' }
  | { status: 'ready'; userName: string }
  | { status: 'error' };

@Component({
  selector: 'app-user-profile',
  standalone: true,
  imports: [AsyncPipe],
  template: `
    @if (state$ | async; as state) {
      @switch (state.status) {
        @case ('loading') { <p>Loading user...</p> }
        @case ('ready') { <p>Hello {{ state.userName }}</p> }
        @case ('error') { <p role="alert">Couldn't load the user.</p> }
      }
    }
  `,
})
export class UserProfile {
  readonly state$ = inject(UserApiService).getUser().pipe(
    map((user): ProfileState => ({
      status: 'ready',
      userName: user.userName,
    })),
    catchError(() => of<ProfileState>({ status: 'error' })),
    startWith<ProfileState>({ status: 'loading' }),
  );
}

Import UserProfile into App, add it to App's imports array, and append <app-user-profile></app-user-profile> to app.html to render this optional extension.

  • startWith() supplies the loading state before the request result.
  • map() converts the response into a ready state; catchError() replaces a failure with an error state.
  • HTTP Observables normally complete after the response. Completion doesn't erase the displayed result.
  • The async pipe subscribes for the template and unsubscribes when the component is destroyed.

These operators make loading and failure explicit, while AsyncPipe manages the subscription.

For a real backend, replace the fixture URL with your API endpoint. The response type argument doesn't validate incoming JSON, so validate untrusted data at the boundary. See our Angular HttpClient tutorial for more request examples, and Angular's HTTP testing guide for testing without real network traffic.

Common Mistakes

  • Providing a shared service in every component. That intentionally creates local instances instead of sharing the root instance.
  • Calling inject() inside ngOnInit. An ordinary lifecycle-hook call isn't an injection context. Inject in a field initializer, then use the dependency in the hook; our ngOnInit example explains lifecycle initialization.
  • Copying a signal's current value once. Keep the signal reference when the UI must follow later changes.
  • Confusing shared state with caching or authentication. Define request reuse, persistence, and security separately.

The core example shares one root-provided SessionService, and the template reads its reactive state. Keep that provider at root for application-wide sharing; move it closer to consumers only when separate instances are intentional.

FAQ

How do you create and inject a service in Angular?

Generate the service with Angular CLI, provide it at the intended scope, and request it with inject(SessionService) in a component field initializer. In this example, @Injectable({ providedIn: 'root' }) registers the service without editing a module.

What does providedIn: 'root' mean in Angular?

It registers the service with the application's root injector. Consumers resolving that provider share its instance, but a component or other child injector can provide a different instance.

Are Angular services always singletons?

No. Instance sharing depends on provider scope, not on a class being called a service. A component-level provider creates an instance for each providing component instance.

Where should an Angular service be provided?

Use a root provider for application-wide dependencies, route providers for a route subtree, and component providers for deliberately isolated state. Choose the narrowest scope that matches the intended sharing and lifetime.

How do you share data between Angular components using a service?

Have the components inject the same service instance and expose changing data through a signal or Observable. Avoid local providers when you want root-level sharing, and avoid copying a changing value into a one-time snapshot.

Further Reading

Written by Sam Erickson

Software engineer. I write every article on StackChief myself. Author of Mastering the Tech Interview. More about me