ScreenPulse organizes functionality into distinct Angular modules with specific responsibilities. This modular approach promotes code reusability, maintainability, and efficient lazy loading.
CoreModule
The CoreModule contains singleton services, guards, interceptors, and application-wide configuration. It should be imported only once in the root AppModule.
Structure
core/
├── guards/
│ └── auth.guard.ts # Route protection
├── interceptors/
│ ├── auth.interceptor.ts # JWT token injection
│ └── error.interceptor.ts # Global error handling
├── services/
│ ├── auth.service.ts # Authentication state
│ └── user.service.ts # User API calls
├── constants/
│ └── featured-media.const.ts
└── core.module.ts
Implementation
src/app/core/core.module.ts
import { NgModule } from '@angular/core' ;
import { CommonModule } from '@angular/common' ;
@ NgModule ({
declarations: [],
imports: [ CommonModule ]
})
export class CoreModule { }
CoreModule is intentionally minimal because services use providedIn: 'root' for automatic singleton behavior. Guards and interceptors are registered in AppModule’s providers array.
Core Services
Core services manage application-wide state and functionality:
src/app/core/services/auth.service.ts
@ Injectable ({
providedIn: 'root'
})
export class AuthService {
private readonly authTokenKey = 'authToken' ;
private readonly userMailKey = 'userMail' ;
private userMailSubject = new BehaviorSubject < string | null >( null );
private userLoggedInSubject = new BehaviorSubject < boolean >( false );
constructor () {
this . userMailSubject . next ( sessionStorage . getItem ( this . userMailKey ));
this . userLoggedInSubject . next (
sessionStorage . getItem ( this . authTokenKey ) !== null
);
}
setUserSession ( user : AuthUser , token : string ) {
this . setAuthToken ( token );
this . setUserMail ( user . email );
this . setUserName ( user . name );
}
isLoggedInObservable () : Observable < boolean > {
return this . userLoggedInSubject . asObservable ();
}
logOut () {
sessionStorage . removeItem ( this . authTokenKey );
sessionStorage . removeItem ( this . userMailKey );
this . userMailSubject . next ( null );
this . userLoggedInSubject . next ( false );
}
}
Guards
Guards protect routes from unauthorized access:
src/app/core/guards/auth.guard.ts
@ Injectable ({
providedIn: 'root'
})
export class AuthGuard implements CanActivate {
constructor (
private authService : AuthService ,
private router : Router ,
private toastService : ToastrService
) { }
canActivate () : Observable < boolean > {
return this . authService . isLoggedInObservable (). pipe (
take ( 1 ),
tap ( loggedIn => {
if ( ! loggedIn ) {
this . toastService . warning (
'You must be logged in to access this page.' ,
'Access Denied'
);
this . router . navigate ([ '/auth/login' ]);
}
})
);
}
}
Interceptors
Interceptors modify HTTP requests and responses globally:
src/app/core/interceptors/auth.interceptor.ts
@ Injectable ()
export class AuthInterceptor implements HttpInterceptor {
constructor (
private router : Router ,
private authService : AuthService
) {}
intercept (
req : HttpRequest < unknown >,
next : HttpHandler
) : Observable < HttpEvent < unknown >> {
const token = this . authService . getAuthToken ();
let authReq = req ;
if ( token ) {
authReq = req . clone ({
setHeaders: { Authorization: `Bearer ${ token } ` }
});
}
return next . handle ( authReq ). pipe (
catchError (( error : HttpErrorResponse ) => {
if ( error . status === 401 ) {
this . authService . logOut ();
this . router . navigate ([ '/auth/login' ]);
}
return throwError (() => error );
})
);
}
}
Interceptors are registered in AppModule using the HTTP_INTERCEPTORS token with multi: true to allow multiple interceptors in the chain.
SharedModule
The SharedModule exports reusable components, directives, pipes, and commonly-used Angular Material modules. It’s imported by feature modules that need these shared resources.
Structure
shared/
├── components/
│ ├── loading-spinner/
│ ├── empty-state/
│ ├── movie-dialog/
│ └── trailer-dialog/
├── directives/
│ └── fallback-images.directive.ts
├── services/
│ ├── omdb/
│ ├── favorites/
│ └── dialog/
├── models/
│ ├── movie.model.ts
│ ├── auth.model.ts
│ └── search.model.ts
└── shared.module.ts
Implementation
src/app/shared/shared.module.ts
import { NgModule } from '@angular/core' ;
import { CommonModule } from '@angular/common' ;
import { FormsModule , ReactiveFormsModule } from '@angular/forms' ;
// Angular Material Modules
import { MatToolbarModule } from '@angular/material/toolbar' ;
import { MatTableModule } from '@angular/material/table' ;
import { MatIconModule } from '@angular/material/icon' ;
import { MatButtonModule } from '@angular/material/button' ;
import { MatCardModule } from '@angular/material/card' ;
// ... more Material modules
import { OmdbService } from './services/omdb/omdb.service' ;
import { MediaItemDialogComponent } from './components/movie-dialog/movie-dialog.component' ;
import { LoadingSpinnerComponent } from './components/loading-spinner/loading-spinner.component' ;
import { EmptyStateComponent } from './components/empty-state/empty-state.component' ;
import { FallbackImagesDirective } from './directives/fallback-images.directive' ;
const MATERIAL_MODULES = [
MatToolbarModule ,
MatTableModule ,
MatIconModule ,
MatButtonModule ,
MatCardModule ,
// ... more modules
];
@ NgModule ({
declarations: [
MediaItemDialogComponent ,
LoadingSpinnerComponent ,
EmptyStateComponent ,
FallbackImagesDirective
],
imports: [
CommonModule ,
FormsModule ,
ReactiveFormsModule ,
... MATERIAL_MODULES
],
providers: [ OmdbService ],
exports: [
... MATERIAL_MODULES ,
MediaItemDialogComponent ,
LoadingSpinnerComponent ,
EmptyStateComponent ,
FallbackImagesDirective
]
})
export class SharedModule { }
Grouping Material modules into a constant array (MATERIAL_MODULES) keeps the module clean and makes it easy to add or remove modules.
LayoutModule
The LayoutModule contains application shell components like the navbar and footer. It’s imported once in AppModule.
src/app/layout/layout.module.ts
import { NgModule } from '@angular/core' ;
import { CommonModule } from '@angular/common' ;
import { RouterModule } from '@angular/router' ;
import { MatToolbarModule } from '@angular/material/toolbar' ;
import { MatIconModule } from '@angular/material/icon' ;
import { MatButtonModule } from '@angular/material/button' ;
import { NavbarComponent } from './navbar/navbar.component' ;
import { FooterComponent } from './footer/footer.component' ;
@ NgModule ({
declarations: [
NavbarComponent ,
FooterComponent
],
imports: [
CommonModule ,
RouterModule ,
MatToolbarModule ,
MatIconModule ,
MatButtonModule
],
exports: [
NavbarComponent ,
FooterComponent
]
})
export class LayoutModule { }
Feature Modules
Feature modules encapsulate specific application features and are lazy-loaded via the router.
Search Module Example
src/app/pages/search/search.module.ts
import { NgModule } from '@angular/core' ;
import { CommonModule } from '@angular/common' ;
import { SearchRoutingModule } from './search-routing.module' ;
import { SearchComponent } from './page/search.component' ;
import { SharedModule } from 'src/app/shared/shared.module' ;
import { SearchCoverComponent } from './components/search-cover/search-cover.component' ;
import { MediaItemResultsTableComponent } from './components/movie-results-table/movie-results-table.component' ;
import { CarouselComponent } from './components/carousel/carousel.component' ;
import { SearchBarComponent } from './components/search-bar/search-bar.component' ;
@ NgModule ({
declarations: [
SearchComponent ,
SearchCoverComponent ,
MediaItemResultsTableComponent ,
CarouselComponent ,
SearchBarComponent
],
imports: [
CommonModule ,
SearchRoutingModule ,
SharedModule ,
FormsModule ,
ReactiveFormsModule
]
})
export class SearchModule { }
Module Import Rules
Import only once in AppModule
Contains singleton services with providedIn: 'root'
Guards and interceptors registered in AppModule providers
Import in every feature module that needs shared resources
Exports reusable components, directives, pipes
Exports commonly-used third-party modules (Material, Forms)
Import only once in AppModule
Contains application shell components
Exports navbar and footer
Never imported directly - only via lazy loading
Import SharedModule for common functionality
Self-contained with own routing module
Module Dependency Graph
Best Practices
Single Responsibility
Each module should have a clear, single purpose (authentication, shared utilities, etc.)
Avoid Circular Dependencies
CoreModule should never import feature modules, and feature modules shouldn’t import each other
Use providedIn: 'root'
For singleton services, use providedIn: 'root' instead of adding to module providers
Export Selectively
Only export what other modules need to use - keep internal components private
Next Steps
Routing Learn how modules are lazy-loaded via routing
Services Deep dive into service architecture
Lazy Loading Understand code splitting and performance
Architecture Return to architecture overview