RizTech Academy logo
RizTech Academy
NestJS FundamentalsLesson 2 of 630 min

Modules

Every NestJS feature is a module. It is the unit of organisation and the thing that decides what can see what — which makes it both the first thing to build and the most common source of "why can't it find my service".

A module

// apps/api/src/products/products.module.ts
import { Module } from "@nestjs/common";
import { ProductsController } from "./products.controller";
import { ProductsService } from "./products.service";

@Module({
  controllers: [ProductsController],
  providers: [ProductsService],
  exports: [ProductsService],
})
export class ProductsModule {}

A class with a decorator. The class body is empty — all the information is in the decorator, which is unusual if you have not seen this style before.

Four properties, and understanding them is most of this lesson:

Property Means
controllers the HTTP endpoints this module owns
providers injectable things available inside this module
imports other modules whose exports this module may use
exports which of this module's providers other modules may use

The rule that catches everyone

A provider is private to its module unless exported.

@Module({
  providers: [ProductsService],      // not exported
})
export class ProductsModule {}
@Module({
  imports: [ProductsModule],
  providers: [OrdersService],
})
export class OrdersModule {}
export class OrdersService {
  constructor(private readonly products: ProductsService) {}   // fails
}
Nest can't resolve dependencies of the OrdersService (?).
Please make sure that the argument ProductsService at index [0]
is available in the OrdersModule context.

That error is long and it tells you exactly what is wrong. Two things must both be true:

  1. ProductsModule exports ProductsService
  2. OrdersModule imports ProductsModule

Miss either and you get that message. When you see it, check both — most people check only the import.

This is not bureaucracy. It means a module's public surface is declared, so you can change anything not exported without wondering who depends on it. Same reasoning as the underscore convention in Python, enforced by the framework.

The root module

// apps/api/src/app.module.ts
import { Module } from "@nestjs/common";
import { ConfigModule } from "@nestjs/config";
import { ProductsModule } from "./products/products.module";
import { OrdersModule } from "./orders/orders.module";
import { PrismaModule } from "./prisma/prisma.module";

@Module({
  imports: [
    ConfigModule.forRoot({ isGlobal: true }),
    PrismaModule,
    ProductsModule,
    OrdersModule,
  ],
})
export class AppModule {}

Every module must be reachable from here, directly or through another module. A module nobody imports simply does not exist at runtime — its controllers register no routes, and the symptom is a 404 on an endpoint you definitely wrote.

404 on a route you know exists? Check the module is imported.

Generating them

cd apps/api
nest g module products
nest g controller products
nest g service products

Or all three:

nest g resource products

That asks whether you want REST, GraphQL or a microservice, and whether to generate CRUD entry points. Choosing REST and yes gives you a module, controller, service, DTOs and a test file, wired together.

The CLI also adds the module to app.module.ts automatically, which is one less thing to forget.

Shared modules

Something several features need — a database client, a mailer — goes in a module that exports it:

// apps/api/src/prisma/prisma.module.ts
import { Global, Module } from "@nestjs/common";
import { PrismaService } from "./prisma.service";

@Global()
@Module({
  providers: [PrismaService],
  exports: [PrismaService],
})
export class PrismaModule {}

@Global() makes the exports available everywhere without importing the module each time. Convenient, and it hides a dependency — nothing in OrdersModule shows that it uses Prisma.

Use @Global() sparingly. Config and the database client are reasonable. Making business modules global defeats the point of having modules at all.

Dynamic modules

Some modules need configuration, which is why you see .forRoot() and .register():

ConfigModule.forRoot({ isGlobal: true })
JwtModule.register({ secret: process.env.JWT_SECRET, signOptions: { expiresIn: "1d" } })

Those are static methods returning a configured module.

For configuration that itself depends on something injectable, the async form:

JwtModule.registerAsync({
  inject: [ConfigService],
  useFactory: (config: ConfigService) => ({
    secret: config.getOrThrow<string>("JWT_SECRET"),
    signOptions: { expiresIn: "1d" },
  }),
})

Convention: forRoot for a module configured once for the whole application, register for one configured per use, and the Async variants when the configuration needs injection.

You will mostly consume these rather than write them.

Organising the Kirana Store

apps/api/src/
    main.ts
    app.module.ts
    prisma/
        prisma.module.ts
        prisma.service.ts
    auth/
        auth.module.ts
        auth.controller.ts
        auth.service.ts
        guards/
        strategies/
    users/
    products/
        products.module.ts
        products.controller.ts
        products.service.ts
        dto/
            create-product.dto.ts
            query-products.dto.ts
    cart/
    orders/
    common/
        filters/
        interceptors/
        decorators/

One module per business concept, not per technical layer. Not controllers/, services/, models/ with everything mixed — everything about products lives in products/, so a change to products touches one folder.

common/ holds genuinely cross-cutting things: the exception filter, the logging interceptor, custom decorators. They are not a feature.

Circular dependencies

OrdersModule needs ProductsModule, and products needs orders to show a sales count:

Nest cannot create the OrdersModule instance.
A circular dependency between modules has been detected.

forwardRef() exists and makes it work:

@Module({
  imports: [forwardRef(() => ProductsModule)],
})

Treat it as a last resort. A circular dependency almost always means the split is wrong — the same conclusion as circular imports in Python. Usually one of three things is true: the shared logic belongs in a third module; one module should not need the other and can take the data as an argument; or the two are really one concept.

Reach for the design fix first.

Check your work

Why a provider must be exported: so a module's public surface is explicit and anything not exported can be changed freely.

The two things needed to use another module's service: that module must export it, and your module must import that module. Both, every time.

Why an endpoint 404s despite existing: its module is not imported anywhere reachable from AppModule, so its controller never registered.

What @Global() costs: it hides the dependency. Nothing in the consuming module declares that it uses the global one.

forRoot versus register: forRoot configures a module once for the whole application; register configures it per use. The Async variants allow the configuration to be injected.

Organise by: business concept, not technical layer — products/, not services/.

What a circular dependency usually means: the module boundary is wrong. forwardRef is a workaround, not a fix.

Practice

  1. Generate a products module, controller and service with the CLI. Confirm the CLI added it to app.module.ts.
  2. Remove it from app.module.ts and call an endpoint. Read the 404 and restore it.
  3. Generate an orders module. Inject ProductsService into OrdersService without exporting it. Read the error in full.
  4. Fix it by exporting only. Confirm it still fails, then add the import.
  5. Create a PrismaModule marked @Global() and inject its service in two modules without importing it.
  6. Remove @Global() and fix the resulting errors properly.
  7. Create a deliberate circular dependency between two modules. Read the error, fix it with forwardRef, then fix it properly by extracting the shared part.
  8. Lay out folders for cart, auth and users following the structure above.

Next: controllers, and mapping URLs to methods.

Stuck on this lesson?

Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.

About the internship