Skip to main content
The core layer contains application-wide functionality that is loaded once when the application starts. This includes authentication, error handling, internationalization, and global layout components.
Location: src/app/core/All services in the core layer are singletons provided at the root level using providedIn: 'root' or configured in app.config.ts.

Core layer structure

src/app/core/
├── language-switch/        # Internationalization
│   ├── language-switch.component.ts
│   └── language-switch.service.ts
├── layout/                 # Layout and error handling
│   ├── cookies.component.ts
│   └── error.service.ts
└── providers/              # Guards and interceptors
    ├── auth.guard.ts
    └── auth.interceptor.ts

Language switching

The application supports multiple languages using @ngx-translate/core.

LanguageSwitchService

Manages language selection and persistence:
src/app/core/language-switch/language-switch.service.ts
import { inject, Injectable } from "@angular/core";
import { TranslateService } from '@ngx-translate/core';

@Injectable({
    providedIn: 'root',
})
export class LanguageSwitchService {
    public static readonly availableLanguages: string[] = ['en', 'es'];
    public static readonly defaultLanguage: string = LanguageSwitchService.availableLanguages[0];

    private translateService: TranslateService = inject(TranslateService);

    public initLanguageFromLocalStorage(): void {
        const languageFromStorage: string | null = localStorage.getItem('language');
        if (
            languageFromStorage &&
            LanguageSwitchService.availableLanguages.includes(languageFromStorage)
        ) {
            this.setLanguage(languageFromStorage);
        }
    }

    public setLanguage(languageId: string): void {
        if (LanguageSwitchService.availableLanguages.includes(languageId)) {
            this.translateService.use(languageId);
            localStorage.setItem('language', languageId);
        }
    }
}
Key features:
  • Stores language preference in localStorage
  • Validates language codes against allowed values
  • Integrates with ngx-translate

Language switch component

Provides UI for language selection:
src/app/core/language-switch/language-switch.component.ts
import { Component, inject } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { LanguageSwitchService } from './language-switch.service';

@Component({
  standalone: true,
  selector: 'app-navbar',
  template: `
    <button (click)="changeLang('en')">English</button>
    <button (click)="changeLang('es')">Spanish</button>
  `
})
export class NavbarComponent {
  private languageSwitchService: LanguageSwitchService = inject(LanguageSwitchService);

  constructor(private translate: TranslateService) {
    translate.setDefaultLang('es');
  }

  changeLang(lang: string) {
    this.languageSwitchService.setLanguage(lang);
  }
}

Translation files

Translation files are stored in src/assets/i18n/:
src/assets/i18n/
├── en.json
└── es.json
src/app/app.config.ts
import { APP_INITIALIZER, ApplicationConfig, importProvidersFrom } from '@angular/core';
import { TranslateModule, TranslateService } from '@ngx-translate/core';
import { initializeTranslation, provideTranslation } from './config/httpLoaderFactory';

export const appConfig: ApplicationConfig = {
  providers: [
    importProvidersFrom([TranslateModule.forRoot(provideTranslation())]),
    {
      provide: APP_INITIALIZER,
      useFactory: initializeTranslation,
      multi: true,
      deps: [TranslateService],
    },
  ]
};

Authentication providers

AuthGuard

Protects routes that require authentication using functional guard pattern:
src/app/core/providers/auth.guard.ts
import { inject } from '@angular/core';
import { CanActivateFn, Router } from '@angular/router';
import { environment } from '@env/environment';
import { AuthStore } from '@services/state/auth.store';

/**
 * Guard function to check if the user is authenticated.
 * @returns True if the user is authenticated. Otherwise the URL tree to redirect to the login page.
 */
export const authGuard: CanActivateFn = () => {
  if (environment.securityOpen) return true;
  const authStore = inject(AuthStore);
  if (authStore.isAuthenticated()) return true;
  const router = inject(Router);
  return router.createUrlTree(['/auth', 'login']);
};
Key features:
  • Uses functional guard pattern (Angular 14+)
  • Checks authentication status via AuthStore
  • Supports security bypass for development
  • Redirects to login on failed authentication
Usage in routes:
import { Routes } from '@angular/router';
import { authGuard } from './core/providers/auth.guard';

export const routes: Routes = [
  {
    path: 'dashboard',
    loadComponent: () => import('./dashboard/dashboard.component'),
    canActivate: [authGuard]
  }
];

AuthInterceptor

Adds authentication headers and handles auth errors:
src/app/core/providers/auth.interceptor.ts
import { HttpHandlerFn, HttpInterceptorFn, HttpRequest } from '@angular/common/http';
import { inject } from '@angular/core';
import { Router } from '@angular/router';
import { NULL_USER_ACCESS_TOKEN } from '@domain/userAccessToken.type';
import { AuthStore } from '@services/state/auth.store';
import { NotificationsStore } from '@services/state/notifications.store';
import { catchError, throwError } from 'rxjs';

const AUTH_ERROR_CODE = 401;

/**
 * Interceptor function to add the Authorization header to the request and handle 401 errors.
 */
export const authInterceptor: HttpInterceptorFn = (req: HttpRequest<unknown>, next: HttpHandlerFn) => {
  const authStore: AuthStore = inject(AuthStore);
  const notificationsStore: NotificationsStore = inject(NotificationsStore);
  const router: Router = inject(Router);

  // Get access token from AuthStore
  const accessToken: string = authStore.accessToken();
  
  // Add the Authorization header to the request
  const authorizationHeader: string = accessToken ? `Bearer ${accessToken}` : '';
  req = req.clone({
    setHeaders: {
      Authorization: authorizationHeader,
    },
  });
  
  // Handle errors
  return next(req).pipe(
    catchError((error) => {
      if (error.status === AUTH_ERROR_CODE) {
        authStore.setState(NULL_USER_ACCESS_TOKEN);
        router.navigate(['/auth', 'login']);
      }
      notificationsStore.addNotification({ message: error.message, type: 'error' });
      return throwError(() => error);
    }),
  );
};
Key features:
  • Uses functional interceptor pattern
  • Injects Bearer token automatically
  • Handles 401 errors by clearing auth state
  • Shows error notifications
  • Redirects to login on auth failure
Registration in app.config.ts:
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { authInterceptor } from './core/providers/auth.interceptor';

export const appConfig: ApplicationConfig = {
  providers: [
    provideHttpClient(withInterceptors([authInterceptor])),
  ]
};

Error handling

ErrorService

Global error handler that displays errors as notifications:
src/app/core/layout/error.service.ts
import { HttpErrorResponse } from '@angular/common/http';
import { ErrorHandler, NgZone, inject } from '@angular/core';
import { Notification } from '@domain/notification.type';
import { NotificationsStore } from '@services/state/notifications.store';

/**
 * Service to handle errors and show notifications.
 * @extends ErrorHandler
 */
class ErrorService extends ErrorHandler {
  #notificationsStore: NotificationsStore = inject(NotificationsStore);
  #zone = inject(NgZone);

  override handleError(error: any): void {
    const notification: Notification = { message: 'An error occurred', type: 'error' };
    
    if (error instanceof HttpErrorResponse) {
      notification.message = error.message;
    } else {
      notification.message = error.toString();
    }
    
    // Run in zone to trigger change detection
    this.#zone.run(() => {
      this.#notificationsStore.addNotification(notification);
    });
    
    // Call default handler
    super.handleError(error);
  }
}

export function provideErrorHandler(): EnvironmentProviders {
  return makeEnvironmentProviders([{ provide: ErrorHandler, useClass: ErrorService }]);
}
Key features:
  • Extends Angular’s ErrorHandler
  • Integrates with NotificationsStore
  • Handles both HTTP and application errors
  • Runs in NgZone for proper change detection
  • Uses provider function for IoC
Error handlers run outside Angular’s zone by default. Always use NgZone.run() when updating state or triggering UI changes.

Layout components

CookiesComponent

Handles cookie consent and privacy notifications (from src/app/core/layout/cookies.component.ts).
Layout components in the core layer are typically used in the root app.component.ts and displayed across all routes.

Adding new core functionality

Follow these steps when adding new app-wide functionality:
1

Determine the category

Decide which core subdirectory fits your functionality:
  • providers/ - Guards, interceptors, resolvers
  • layout/ - Shell components, error handlers
  • language-switch/ - i18n related functionality
2

Create the service or provider

ng generate service core/providers/my-new-guard
Use providedIn: 'root' for services:
@Injectable({ providedIn: 'root' })
export class MyService { }
3

Register in app.config.ts

For guards, interceptors, and error handlers:
export const appConfig: ApplicationConfig = {
  providers: [
    provideHttpClient(withInterceptors([myInterceptor])),
    provideErrorHandler(),
  ]
};
4

Use in routes or components

Apply guards to routes:
{ path: 'admin', canActivate: [adminGuard], ... }

Best practices

Single responsibility

Each service should have one clear purpose. Split complex logic into multiple services.

Avoid side effects

Core services should be stateless when possible. Use stores (in shared layer) for state management.

Use functional patterns

Prefer functional guards and interceptors over class-based ones for better tree-shaking.

Document dependencies

Clearly document which stores or services your core functionality depends on.
The core layer should never import from the features layer. It can import from the shared layer for domain types and utilities.

Next steps

Shared layer

Learn about reusable components and state management

Folder structure

Review the complete project structure

Build docs developers (and LLMs) love