Skip to main content

Command Palette

Search for a command to run...

#3 — Modules, Controllers & Services (Concepts + Examples)

Updated
5 min read
M

As a former 3D Animator with more than 12 years of experience, I have always been fascinated by the intersection of technology and creativity. That's why I recently shifted my career towards MERN stack development and software engineering, where I have been serving since 2021.

With my background in 3D animation, I bring a unique perspective to software development, combining creativity and technical expertise to build innovative and visually engaging applications. I have a passion for learning and staying up-to-date with the latest technologies and best practices, and I enjoy collaborating with cross-functional teams to solve complex problems and create seamless user experiences.

In my current role as a MERN stack developer, I have been responsible for developing and implementing web applications using MongoDB, Express, React, and Node.js. I have also gained experience in Agile development methodologies, version control with Git, and cloud-based deployment using platforms like Heroku and AWS.

I am committed to delivering high-quality work that meets the needs of both clients and end-users, and I am always seeking new challenges and opportunities to grow both personally and professionally.

1) What is a Module?

A module is a cohesive package of functionality. In NestJS, every feature lives inside a module.

  • Think: UsersModule for user-related concerns, PostsModule for blog posts, etc.

  • Each module has a single entry file: *.module.ts (e.g., users.module.ts).

  • Modules group their own controllers, services, entities/schemas, and tests.

Why modules?

  • ✅ Clear boundaries per feature

  • ✅ Easier scaling in teams

  • ✅ Reuse via imports/exports

  • ✅ Testability (mock isolated providers)


2) The Root of it all: main.tsAppModule → your feature modules

(main.ts)  →  (AppModule)  →  (UsersModule, PostsModule, ...)
  • main.ts boots the Nest app.

  • AppModule is the root module that imports your feature modules.

  • Feature modules can import each other when needed.

Minimal bootstrap (src/main.ts)

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  await app.listen(3000);
}
bootstrap();

Root module (src/app.module.ts)

import { Module } from '@nestjs/common';
import { UsersModule } from './users/users.module';

@Module({
  imports: [UsersModule], // add feature modules here
})
export class AppModule {}

3) Module Anatomy

A module is defined with the @Module() decorator and four key slots:

@Module({
  imports: [],      // other modules this module depends on
  controllers: [],  // route handlers (HTTP layer)
  providers: [],    // services, repositories, guards, interceptors, pipes
  exports: [],      // subset of providers made available to other modules
})
export class FeatureModule {}

Controllers handle routing and I/O.
Services hold business logic and data access.
Providers are injectable classes (DI) — services, guards, interceptors, pipes, factories.


4) Example: Users feature module

users.module.ts

import { Module } from '@nestjs/common';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';

@Module({
  controllers: [UsersController],
  providers: [UsersService],
  exports: [UsersService], // export if other modules need it
})
export class UsersModule {}

users.controller.ts

import { Controller, Get, Param } from '@nestjs/common';
import { UsersService } from './users.service';

@Controller('users')
export class UsersController {
  constructor(private readonly usersService: UsersService) {}

  @Get(':id')
  findOne(@Param('id') id: string) {
    return this.usersService.findOne(id);
  }
}

users.service.ts

import { Injectable } from '@nestjs/common';

@Injectable()
export class UsersService {
  private users = [
    { id: '1', name: 'Ada' },
    { id: '2', name: 'Grace' },
  ];

  findOne(id: string) {
    return this.users.find(u => u.id === id);
  }
}

Tests (e.g., users.controller.spec.ts) live alongside to validate controller behavior; E2E tests go in /test.


5) Wiring Multiple Modules Together

Suppose PostsModule needs UsersService (e.g., to verify a post’s author).

posts.module.ts

import { Module } from '@nestjs/common';
import { PostsService } from './posts.service';
import { PostsController } from './posts.controller';
import { UsersModule } from '../users/users.module';

@Module({
  imports: [UsersModule], // we depend on UsersService exported by UsersModule
  controllers: [PostsController],
  providers: [PostsService],
})
export class PostsModule {}

By putting UsersService in exports of UsersModule, any importing module (like PostsModule) can inject it.

Diagram

main.ts
  └─ AppModule
      ├─ UsersModule  (exports: UsersService)
      └─ PostsModule  (imports: UsersModule) → can inject UsersService

6) Controller vs Service (mental model)

  • Controller: “When an HTTP request hits /users/:id, what do we do?” → parse params, delegate to service.

  • Service: “How do we fetch/process data?” → business rules, DB calls, integrations.

Keeping controllers thin and services rich makes your app testable and maintainable.


7) Entities/Schemas inside a Module

Depending on your persistence layer:

  • TypeORMuser.entity.ts

  • Mongooseuser.schema.ts

These files describe how data is stored (think: Model in MVC). They are typically provided via the ORM module (e.g., TypeOrmModule.forFeature([...]) or MongooseModule.forFeature([...])) inside your feature module’s imports.


8) Generating Modules & Files via CLI

# module only
nest g module users

# controller inside users
nest g controller users

# service inside users
nest g service users

# full CRUD resource bundle (module + controller + service + DTOs)
nest g resource posts

# simulate before actually writing files
nest g controller users --dry-run

9) Best Practices & Pitfalls

  • Single-responsibility feature modules (Users, Posts, Auth, Mail, Files…) keep boundaries clean.

  • Export only what’s needed. Keep provider surfaces small.

  • Avoid circular imports. If unavoidable, use forwardRef(() => OtherModule).

  • Shared utilities: create a SharedModule for common pipes/guards and export them.

  • Provider scope: defaults to singleton per app/module context — good for stateless services.

  • Don’t re-import the same module in many places if you can expose required providers via exports from one place.

  • Testing: mock services in controller tests; do E2E for integration behaviour.


10) Quick Quiz (2 min)

  1. What are the four arrays in @Module() and what do they do?

  2. Where do you place HTTP route handlers? Where should business logic live?

  3. How does one module use a provider from another module?

  4. What’s a simple way to preview what the CLI will generate without touching the disk?

(Answers: controllers/providers/imports/exports; controllers vs services; export provider in source module and import that module; use --dry-run.)


11) Practice Tasks

  1. Create users and posts modules.
nest g module users
nest g module posts
  1. Add a controller + service to users and return a mock user by id.
nest g controller users
nest g service users
  1. Export UsersService from UsersModule and import UsersModule into PostsModule.

  2. Add a POST /posts endpoint that validates an authorId by calling UsersService.findOne.

  3. (Optional) Add unit tests for UsersController with a mocked UsersService.