Skip to main content
Jet’s services follow Angular best practices with dependency injection, signal-based state management, and clear separation of concerns.

Service Structure

Services use the modern inject() function and signal-based state:
@Injectable()
export class UserService {
  readonly #activatedRoute = inject(ActivatedRoute);
  readonly #supabaseClient = inject(SUPABASE_CLIENT);
  readonly #loggerService = inject(LoggerService);

  readonly #user: WritableSignal<null | User>;

  public constructor() {
    this.#user = signal(null);

    this.#supabaseClient.auth.onAuthStateChange(
      (_authChangeEvent: AuthChangeEvent, authSession: AuthSession | null): void => {
        this.#user.set(authSession?.user ?? null);
      },
    );

    this.#loggerService.logServiceInitialization('UserService');
  }

  public get user(): Signal<null | User> {
    return this.#user.asReadonly();
  }
}

Service Scopes

Root-Scoped Services

Services available throughout the application:
@Injectable({ providedIn: 'root' })
export class SettingsService {
  readonly #loggerService = inject(LoggerService);
  readonly #storageService = inject(StorageService);

  readonly #settings: WritableSignal<Settings>;
  public readonly directionality: Signal<Settings['languageOption']['directionality']>;

  public constructor() {
    const storedSettings: null | Settings = this.#storageService.getLocalStorageItem<Settings>(
      LocalStorageKey.Settings,
    );

    this.#settings = signal({ ...DEFAULT_SETTINGS, ...storedSettings });
    this.directionality = computed(() => this.#settings().languageOption.directionality);

    effect(
      () => {
        this.#loggerService.logEffectRun('settings');
        const settings: Settings = this.#settings();
        untracked(() =>
          this.#storageService.setLocalStorageItem(LocalStorageKey.Settings, settings),
        );
      },
      { debugName: 'settings' },
    );

    this.#loggerService.logServiceInitialization('SettingsService');
  }

  public get settings(): Signal<Settings> {
    return this.#settings.asReadonly();
  }

  public updateSettings(partialSettings: Partial<Settings>): void {
    this.#settings.update((settings) => ({ ...settings, ...partialSettings }));
  }
}

Route-Scoped Services

Services provided at the route level for scoped data:
export const lazyRoutes: Routes = [
  { 
    children: [...mainRoutes, ...userRoutes], 
    path: '', 
    providers: [UserService, ProfileService] 
  },
];

Service Patterns

State Management with Signals

@Injectable({ providedIn: 'root' })
export class ProgressBarService {
  readonly #loggerService = inject(LoggerService);

  readonly #progressBarConfiguration: WritableSignal<ProgressBarConfiguration>;
  #queueTimeoutId: number | undefined;

  public constructor() {
    this.#progressBarConfiguration = signal({
      bufferValue: 0,
      isVisible: false,
      mode: 'indeterminate',
      value: 0,
    });

    this.#queueTimeoutId = undefined;
    this.#loggerService.logServiceInitialization('ProgressBarService');
  }

  public get progressBarConfiguration(): Signal<ProgressBarConfiguration> {
    return this.#progressBarConfiguration.asReadonly();
  }

  public hideProgressBar(): void {
    this.#queueConfiguration({ isVisible: false });
  }

  public showIndeterminateProgressBar(): void {
    this.#queueConfiguration({ isVisible: true, mode: 'indeterminate' });
  }

  public showQueryProgressBar(): void {
    this.#queueConfiguration({ isVisible: true, mode: 'query' });
  }

  #queueConfiguration(partialProgressBarConfiguration: Partial<ProgressBarConfiguration>): void {
    clearTimeout(this.#queueTimeoutId);

    this.#queueTimeoutId = setTimeout(() => {
      this.#progressBarConfiguration.update((progressBarConfiguration) => ({
        ...progressBarConfiguration,
        ...partialProgressBarConfiguration,
      }));
    }, 90);
  }
}

Data Services with Supabase

@Injectable()
export class ProfileService {
  readonly #supabaseClient = inject(SUPABASE_CLIENT);
  readonly #loggerService = inject(LoggerService);
  readonly #userService = inject(UserService);

  readonly #user: Signal<null | User>;

  public constructor() {
    this.#user = this.#userService.user;
    this.#loggerService.logServiceInitialization('ProfileService');
  }

  public selectProfile() {
    return this.#supabaseClient
      .from(SupabaseTable.Profiles)
      .select()
      .eq('user_id', this.#user()?.id)
      .single()
      .throwOnError();
  }

  public updateAndSelectProfile(partialProfile: Partial<Profile>) {
    return this.#supabaseClient
      .from(SupabaseTable.Profiles)
      .update(partialProfile)
      .eq('user_id', this.#user()?.id)
      .select()
      .single()
      .throwOnError();
  }

  public uploadAvatar(
    file: File,
  ): Promise<
    | { data: { fullPath: string; id: string; path: string }; error: null }
    | { data: null; error: StorageError }
  > {
    const fileExtension: string | undefined = file.name.split('.').pop();
    const timestamp: number = Date.now();
    const path: string = `${this.#userService.user()?.id}/avatar-${timestamp}.${fileExtension}`;

    return this.#supabaseClient.storage.from(SupabaseStorage.ProfileAvatars).upload(path, file);
  }
}

Authentication Service

@Injectable()
export class UserService {
  readonly #activatedRoute = inject(ActivatedRoute);
  readonly #supabaseClient = inject(SUPABASE_CLIENT);
  readonly #loggerService = inject(LoggerService);

  readonly #user: WritableSignal<null | User>;

  public constructor() {
    this.#user = signal(null);

    this.#supabaseClient.auth.onAuthStateChange(
      (_authChangeEvent: AuthChangeEvent, authSession: AuthSession | null): void => {
        this.#user.set(authSession?.user ?? null);
      },
    );

    this.#loggerService.logServiceInitialization('UserService');
  }

  public signInWithPassword(email: string, password: string): Promise<AuthTokenResponsePassword> {
    return this.#supabaseClient.auth.signInWithPassword({ email, password });
  }

  public signInWithOtp(email: string): Promise<AuthOtpResponse> {
    return this.#supabaseClient.auth.signInWithOtp({
      email,
      options: { emailRedirectTo: this.#getRedirectUrlWithReturnUrl(), shouldCreateUser: false },
    });
  }

  public signOut(): Promise<{ error: AuthError | null }> {
    return this.#supabaseClient.auth.signOut();
  }

  public signUp(email: string, password: string): Promise<AuthResponse> {
    return this.#supabaseClient.auth.signUp({
      email,
      options: { emailRedirectTo: this.#getRedirectUrlWithReturnUrl() },
      password,
    });
  }
}

Storage Services

Abstract browser storage APIs:
@Injectable({ providedIn: 'root' })
export class StorageService {
  readonly #loggerService = inject(LoggerService);
  readonly #store2: StoreType;

  public constructor() {
    this.#store2 = store2.namespace('jet');
    this.#loggerService.logServiceInitialization('StorageService');
  }

  public getLocalStorageItem<T>(localStorageKey: LocalStorageKey): null | T {
    return this.#store2.get(localStorageKey);
  }

  public setLocalStorageItem<T>(localStorageKey: LocalStorageKey, data: T): void {
    this.#store2.set(localStorageKey, data);
  }

  public removeLocalStorageItem(localStorageKey: LocalStorageKey): void {
    this.#store2.remove(localStorageKey);
  }

  public clearLocalStorage(): void {
    store2.clearAll();
  }
}

Alert Services

User notifications and feedback:
@Injectable({ providedIn: 'root' })
export class AlertService {
  readonly #matSnackBar = inject(MatSnackBar);
  readonly #translocoService = inject(TranslocoService);
  readonly #loggerService = inject(LoggerService);
  readonly #settingsService = inject(SettingsService);

  readonly #directionality: Signal<Settings['languageOption']['directionality']>;

  public constructor() {
    this.#directionality = this.#settingsService.directionality;
    this.#loggerService.logServiceInitialization('AlertService');
  }

  public showAlert(
    message: string,
    cta: string = this.#translocoService.translate('alerts.ok'),
    action?: () => void,
  ): void {
    const matSnackBarRef: MatSnackBarRef<TextOnlySnackBar> = this.#matSnackBar.open(message, cta, {
      direction: this.#directionality(),
    });

    if (action) {
      matSnackBarRef
        .onAction()
        .pipe(take(1))
        .subscribe(() => {
          action();
        });
    }
  }

  public showErrorAlert(
    message: string = this.#translocoService.translate('alerts.something-went-wrong'),
  ): void {
    this.showAlert(message);
  }
}

Logging Service

Centralized logging with conditional output:
@Injectable({ providedIn: 'root' })
export class LoggerService {
  readonly #isLoggingEnabled = inject(IS_LOGGING_ENABLED);

  public logServiceInitialization(serviceName: string): void {
    if (!this.#isLoggingEnabled) {
      return;
    }
    console.info(`Service ${serviceName} initialized.`);
  }

  public logComponentInitialization(componentName: string): void {
    if (!this.#isLoggingEnabled) {
      return;
    }
    console.debug(`Component ${componentName} initialized.`);
  }

  public logError(error: Error): void {
    if (!this.#isLoggingEnabled) {
      return;
    }
    console.error(error);
  }

  public logException(exception: unknown): void {
    if (!this.#isLoggingEnabled) {
      return;
    }
    console.error(exception);
  }
}

Custom Injection Tokens

Jet uses injection tokens for configuration:
readonly #supabaseClient = inject(SUPABASE_CLIENT);
readonly #isLoggingEnabled = inject(IS_LOGGING_ENABLED);
Services are automatically provided in the dependency injection tree based on their @Injectable decorator configuration.

Service Best Practices

  1. Use Signals: Prefer signals over BehaviorSubjects for state management
  2. Readonly Signals: Expose readonly signals to prevent external mutations
  3. Private Fields: Use # prefix for private fields and methods
  4. Logging: Log service initialization for debugging
  5. Type Safety: Always type service methods and return values
  6. Error Handling: Services should throw or return errors, not handle them
  7. Single Responsibility: Each service should have a clear, focused purpose
Avoid circular dependencies between services. Use injection tokens or restructure service hierarchy if needed.

Build docs developers (and LLMs) love