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:
ProductsModuleexportsProductsServiceOrdersModuleimportsProductsModule
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
- Generate a
productsmodule, controller and service with the CLI. Confirm the CLI added it toapp.module.ts. - Remove it from
app.module.tsand call an endpoint. Read the 404 and restore it. - Generate an
ordersmodule. InjectProductsServiceintoOrdersServicewithout exporting it. Read the error in full. - Fix it by exporting only. Confirm it still fails, then add the import.
- Create a
PrismaModulemarked@Global()and inject its service in two modules without importing it. - Remove
@Global()and fix the resulting errors properly. - Create a deliberate circular dependency between two modules. Read the error,
fix it with
forwardRef, then fix it properly by extracting the shared part. - Lay out folders for
cart,authandusersfollowing 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