Welcome to this lesson on Modules in NestJS. So far, we've explored how to set up a basic NestJS application, delved into the roles of key files, and understood the concepts of controllers and providers. In our last lesson, we covered the essentials of providers and how they work with dependency injection. This lesson will build on that foundation by examining how modules in NestJS help structure your application, making it organized, scalable, and maintainable.
Before diving in, let's recap the concept of Dependency Injection (DI
), which we touched upon in the last lesson. DI
is a design pattern used to implement IoC (Inversion of Control), allowing dependencies to be injected into a class rather than the class creating them itself. This improves code modularity and ease of testing.
In NestJS, DI
is implemented through the use of decorators like @Injectable()
and types that indicate what should be injected. Modules in NestJS group related components, like services and controllers, together. This modular approach keeps your code organized and helps manage dependencies effectively.
A module is defined using the @Module()
decorator, which takes a metadata object. This object typically includes arrays of providers, controllers, and other imported modules.
Here's a simple example:
TypeScript1import { Module } from '@nestjs/common'; 2import { BooksController } from './books.controller'; 3import { BooksService } from './books.service'; 4 5@Module({ 6 controllers: [BooksController], 7 providers: [BooksService], 8}) 9export class BooksModule {}
This code snippet shows BooksModule
registering BooksController
and BooksService
. By doing this, we keep related components together, making the application more organized.
Next, let's introduce a service to fetch book ratings. We'll name this service BookRatingsService
. Services in NestJS are annotated with @Injectable()
to mark them as providers that can be injected into other components.
Here’s how we define the BookRatingsService
:
TypeScript1import { Injectable } from '@nestjs/common'; 2 3const ratings = { 4 "1": 4, 5 "2": 5, 6 "3": 3, 7}; 8 9@Injectable() 10export class BookRatingsService { 11 getRatingForBook(id: string): number { 12 return ratings[id] ?? 0; 13 } 14}
In this example:
- We use the
@Injectable()
decorator to allowBookRatingsService
to be injected as a dependency into other classes. - The
getRatingForBook
method returns the rating for a specific book ID from theratings
object. If the book ID doesn't exist in theratings
object, it returns 0 by default.
Now let's integrate the BookRatingsService
with BooksService
. The goal is to fetch book ratings when retrieving book data.
Here's how you can achieve this:
TypeScript1import { Injectable } from '@nestjs/common'; 2import { BookRatingsService } from './bookRatings.service'; 3import { Book, books } from './books.data'; 4 5@Injectable() 6export class BooksService { 7 constructor(private readonly bookRatingsService: BookRatingsService) {} 8 9 getAllBooks(): Book[] { 10 return books.map(book => ({ 11 ...book, 12 rating: this.bookRatingsService.getRatingForBook(book.id), 13 })); 14 } 15}
In this example:
- The
BooksService
class has a dependency onBookRatingsService
, injected via the constructor. - The
getAllBooks
method now includes a rating for each book, fetched usingBookRatingsService
.
We need to register our BookRatingsService
within the BooksModule
so that it can be injected into BooksService
.
Here’s how you configure BooksModule
:
TypeScript1import { Module } from '@nestjs/common'; 2import { BooksController } from './books.controller'; 3import { BooksService } from './books.service'; 4import { BookRatingsService } from './bookRatings.service'; 5 6@Module({ 7 controllers: [BooksController], 8 providers: [ 9 BooksService, 10 BookRatingsService, 11 ], 12}) 13export class BooksModule {}
By registering both BooksService and BookRatingsService in the BooksModule
, we are grouping related services that handle book-related logic together. This approach keeps related functionality in one place, making the module easier to maintain and test.
Once you’ve created a module like BooksModule
, it can easily be reused in different parts of your application. For example, if you later need to build a AuthorsModule
, it could follow the same structure, allowing you to build a highly modular and maintainable system.
Finally, let's integrate our BooksModule
into the main AppModule
. This step makes the BooksModule
a part of the overall application. The existing top-level AppModule
is still there, serving its initial purpose, but now we're also including everything in the BooksModule
so both sets of routes are exposed. This means that any endpoints defined in BooksController
will be accessible alongside any other routes defined in the AppController
or any other modules you might add in the future.
Here’s how you do it:
TypeScript1import { Module } from '@nestjs/common'; 2import { AppController } from './app.controller'; 3import { AppService } from './app.service'; 4import { BooksModule } from './books/books.module'; 5 6@Module({ 7 imports: [BooksModule], 8 controllers: [AppController], 9 providers: [AppService], 10}) 11export class AppModule {}
In this example:
- The
BooksModule
is added to theimports
array ofAppModule
, making it part of the main application. - This allows NestJS to recognize and use the controllers and providers defined within
BooksModule
.
In this lesson, we covered the creation and integration of modules in NestJS. We implemented a BookRatingsService
and integrated it with BooksService
, encapsulating these services within BooksModule
, which was then integrated into the main AppModule
.
Understanding modules is crucial for building scalable and maintainable NestJS applications. Modules help organize your code and manage dependencies effectively.
Next, you'll get hands-on practice with these concepts through various exercises designed to reinforce your understanding. These exercises will help you apply what you've learned and build confidence in working with NestJS modules. Happy coding!