1. A browser storage service for the client

Applications running on the server cannot access browser-specific globals like window, document, navigator, or location. Certain HTMLElement properties are likewise unavailable in server-side contexts.

Code that depends on these browser-only symbols should always execute exclusively on the client. The afterRender and afterNextRender lifecycle hooks are ideal for this, since Angular skips them on the server and only runs them in the browser.

However, these hooks won't help when the dependency sits inside a service — for example, when you need to access localStorage from an injectable. This guide walks through a solution that keeps such services functional in SSR environments.

First, create a BrowserStorageService with the implementation below:

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

export const BROWSER_STORAGE = new InjectionToken<Storage>('Browser Storage', {
  providedIn: 'root',
  factory: () => localStorage,
});

@Injectable()
export class BrowserStorageService {
  constructor(@Inject(BROWSER_STORAGE) public storage: Storage) {}

  get(key: string) {
    return this.storage.getItem(key);
  }

  set(key: string, value: string) {
    this.storage.setItem(key, value);
  }

  remove(key: string) {
    this.storage.removeItem(key);
  }

  clear() {
    this.storage.clear();
  }
}
Enter fullscreen mode Exit fullscreen mode

This service encapsulates all direct browser storage interactions.

2. Registering the service at the application root

Add BrowserStorageService to the providers array. In standalone projects, this means updating app.config.ts; for módulo-based setups, this is app.module.ts.

providers: [BrowserStorageService],
Enter fullscreen mode Exit fullscreen mode

Going forward, use BrowserStorageService instead of reaching for localStorage directly throughout the application.

At this stage, server-side handling is still missing. Building the project will likely fail with an error similar to this:

ERROR ReferenceError: localStorage is not defined
Enter fullscreen mode Exit fullscreen mode

We'll fix this next.

3. A server-safe storage service

The server has no localStorage global, so the app crashes when it instantiates BrowserStorageService during SSR.

The trick is to give the server its own implementation that can run without crashing.

Create a second service, BrowserStorageServerService, with the following content:

import { Injectable } from '@angular/core';
import { BrowserStorageService } from './browser-storage.service';

@Injectable()
export class BrowserStorageServerService extends BrowserStorageService {
  constructor() {
    super({
      clear: () => {},
      getItem: (key: string) => JSON.stringify({ key }),
      setItem: (key: string, value: string) => JSON.stringify({ [key]: value }),
      key: (index: number) => index.toString(),
      length: 0,
      removeItem: (key: string) => JSON.stringify({ key }),
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

Notice that BrowserStorageServerService extends BrowserStorageService, which makes overriding methods simple and type-safe.

This is a stub implementation. What matters is that every method from the base class is present; the logic can be a placeholder and adjusted later.

4. Providing the server version

Next, instruct the server to use BrowserStorageServerService whenever BrowserStorageService is injected. The provide and useClass properties of ClassProvider make this straightforward.

Update the providers array in app.config.server.ts for standalone apps, or in app.server.module.ts for module-based ones:

providers: [
    {
      provide: BrowserStorageService,
      useClass: BrowserStorageServerService,
    },
  ],
Enter fullscreen mode Exit fullscreen mode

That's all. The application should now run seamlessly on both server and client.